Search/cache-images.go

325 lines
8 KiB
Go
Raw Normal View History

2024-10-13 00:04:46 +02:00
package main
import (
"bytes"
"crypto/tls"
2024-10-13 00:04:46 +02:00
"encoding/json"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
2024-10-13 00:04:46 +02:00
"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
invalidImageIDs = make(map[string]struct{})
invalidImageIDsMu sync.Mutex
2024-10-13 00:04:46 +02:00
)
func cacheImage(imageURL, filename, imageID string) (string, bool, error) {
2024-10-13 00:04:46 +02:00
cacheDir := "image_cache"
cachedImagePath := filepath.Join(cacheDir, filename)
// Check if the image is already cached
if _, err := os.Stat(cachedImagePath); err == nil {
return cachedImagePath, true, nil
2024-10-13 00:04:46 +02:00
}
// 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, true, nil
2024-10-13 00:04:46 +02:00
}
cachingSemaphore <- struct{}{} // Acquire a token
defer func() { <-cachingSemaphore }() // Release the token
// Create a custom http.Client that skips SSL certificate verification
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Download the image using the custom client
resp, err := client.Get(imageURL)
2024-10-13 00:04:46 +02:00
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
2024-10-13 00:04:46 +02:00
}
defer resp.Body.Close()
// Read the image data into a byte slice
data, err := io.ReadAll(resp.Body)
if err != nil {
recordInvalidImageID(imageID)
return "", false, err
2024-10-13 00:04:46 +02:00
}
// Check if the response is actually an image
2024-10-13 00:04:46 +02:00
contentType := http.DetectContentType(data)
if !strings.HasPrefix(contentType, "image/") {
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("URL did not return an image: %s", imageURL)
2024-10-13 00:04:46 +02:00
}
// 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 {
recordInvalidImageID(imageID)
return "", false, err
2024-10-13 00:04:46 +02:00
}
// Clean up mutex
cachingImagesMu.Lock()
delete(cachingImages, imageURL)
cachingImagesMu.Unlock()
return cachedImagePath, true, nil
2024-10-13 00:04:46 +02:00
}
// 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:
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("unsupported image type: %s", contentType)
2024-10-13 00:04:46 +02:00
}
if err != nil {
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("failed to decode image: %v", err)
2024-10-13 00:04:46 +02:00
}
// 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 {
recordInvalidImageID(imageID)
return "", false, err
2024-10-13 00:04:46 +02:00
}
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 {
recordInvalidImageID(imageID)
return "", false, err
2024-10-13 00:04:46 +02:00
}
// Clean up mutex
cachingImagesMu.Lock()
delete(cachingImages, imageURL)
cachingImagesMu.Unlock()
return cachedImagePath, true, nil
2024-10-13 00:04:46 +02:00
}
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) {
printDebug("Cached image not found: %s, serving missing.svg", cachedImagePath)
// Serve missing image
missingImagePath := filepath.Join("static", "images", "missing.svg")
w.Header().Set("Content-Type", "image/svg+xml")
http.ServeFile(w, r, missingImagePath)
return
} else if err != nil {
printWarn("Error checking image file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
2024-10-13 00:04:46 +02:00
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)
invalidImageIDsMu.Lock()
defer invalidImageIDsMu.Unlock()
2024-10-13 00:04:46 +02:00
for _, id := range ids {
// Check if the image ID is in the invalidImageIDs map
if _, invalid := invalidImageIDs[id]; invalid {
// Image is invalid, set status to "missing"
statusMap[id] = "/static/images/missing.svg"
continue
}
2024-10-14 22:15:38 +02:00
// 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
}
}
2024-10-13 00:04:46 +02:00
2024-10-14 22:15:38 +02:00
if found {
statusMap[id] = cachedImagePath
2024-10-13 00:04:46 +02:00
} else {
// Image is not ready yet
2024-10-13 00:04:46 +02:00
statusMap[id] = ""
}
}
printDebug("Status map: %v", statusMap)
2024-10-13 00:04:46 +02:00
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(statusMap)
}
func recordInvalidImageID(imageID string) {
invalidImageIDsMu.Lock()
defer invalidImageIDsMu.Unlock()
invalidImageIDs[imageID] = struct{}{}
printDebug("Recorded invalid image ID: %s", imageID)
}
func filterValidImages(imageResults []ImageSearchResult) []ImageSearchResult {
invalidImageIDsMu.Lock()
defer invalidImageIDsMu.Unlock()
var filteredResults []ImageSearchResult
for _, img := range imageResults {
if _, invalid := invalidImageIDs[img.ID]; !invalid {
filteredResults = append(filteredResults, img)
} else {
printDebug("Filtering out invalid image ID: %s", img.ID)
}
}
return filteredResults
}
func removeImageResultFromCache(query string, page int, safe bool, lang string, imageID string) {
cacheKey := CacheKey{
Query: query,
Page: page,
Safe: safe,
Lang: lang,
Type: "image",
}
rc := resultsCache
rc.mu.Lock()
defer rc.mu.Unlock()
keyStr := rc.keyToString(cacheKey)
item, exists := rc.results[keyStr]
if !exists {
return
}
// Filter out the image with the given ID
var newResults []SearchResult
for _, r := range item.Results {
if imgResult, ok := r.(ImageSearchResult); ok {
if imgResult.ID != imageID {
newResults = append(newResults, r)
} else {
printDebug("Removing invalid image ID from cache: %s", imageID)
}
} else {
newResults = append(newResults, r)
}
}
// Update or delete the cache entry
if len(newResults) > 0 {
rc.results[keyStr] = CachedItem{
Results: newResults,
StoredTime: item.StoredTime,
}
} else {
delete(rc.results, keyStr)
}
}