Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var (
PrefetchForeground bool // Standalone prefetch, prefetch and exit
AllowNonImage bool
Config = NewWebPConfig()
Version = "0.15.0"
Version = "0.15.1"
WriteLock = cache.New(5*time.Minute, 10*time.Minute)
ConvertLock = cache.New(5*time.Minute, 10*time.Minute)
LocalHostAlias = "local"
Expand Down
6 changes: 5 additions & 1 deletion encoder/prefetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ func PrefetchImages() {
}

// RawImagePath string, ImgFilename string, reqURI string
metadata := helper.ReadMetadata(picAbsPath, "", config.LocalHostAlias)
metadata, err := helper.ReadMetadata(picAbsPath, "", config.LocalHostAlias)
if err != nil {
log.Warnf("failed to read metadata for %s, skipping prefetch: %s", picAbsPath, err)
return nil
}
avifAbsPath, webpAbsPath, jxlAbsPath := helper.GenOptimizedAbsPath(metadata, config.LocalHostAlias)

// Using avifAbsPath here is the same as using webpAbsPath/jxlAbsPath
Expand Down
17 changes: 14 additions & 3 deletions handler/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ func fetchRemoteImg(url string, subdir string) (metaContent config.MetaFile) {
}
}

metadata := helper.ReadMetadata(url, etag, subdir)
metadata, err := helper.ReadMetadata(url, etag, subdir)
if err != nil {
log.Warnf("failed to read metadata for %s, rebuilding directly: %s", url, err)
metadata, err = helper.WriteMetadata(url, etag, subdir)
if err != nil {
log.Warnf("failed to write metadata for %s: %s", url, err)
}
}
remoteFileExtension := path.Ext(url)
localRawImagePath := path.Join(config.Config.RemoteRawPath, subdir, metadata.Id) + remoteFileExtension
localExhaustImagePath := path.Join(config.Config.ExhaustPath, subdir, metadata.Id)
Expand All @@ -113,14 +120,18 @@ func fetchRemoteImg(url string, subdir string) (metaContent config.MetaFile) {
// remote file has changed
log.Info("Remote file changed, updating metadata and fetching image source...")
helper.DeleteMetadata(url, subdir)
helper.WriteMetadata(url, etag, subdir)
if _, err := helper.WriteMetadata(url, etag, subdir); err != nil {
log.Warnf("failed to update metadata for changed remote file %s: %s", url, err)
}
} else {
// local file not exists
log.Info("Remote file not found in remote-raw, re-fetching...")
}
_ = downloadFile(localRawImagePath, url)
// Update metadata with newly downloaded file
helper.WriteMetadata(url, etag, subdir)
if _, err := helper.WriteMetadata(url, etag, subdir); err != nil {
log.Warnf("failed to update metadata after downloading %s: %s", url, err)
}
}
return metadata
}
Expand Down
15 changes: 13 additions & 2 deletions handler/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func Convert(c *fiber.Ctx) error {
}

var (
err error
reqHostname = c.Hostname()
reqHost = c.Protocol() + "://" + reqHostname // http://www.example.com:8000
reqHeader = &c.Request().Header
Expand Down Expand Up @@ -126,11 +127,21 @@ func Convert(c *fiber.Ctx) error {
}

if !state.isRemote() {
metadata = helper.ReadMetadata(state.reqURIWithQuery, "", state.targetHostName)
metadata, err = helper.ReadMetadata(state.reqURIWithQuery, "", state.targetHostName)
if err != nil {
log.Warnf("failed to read metadata for %s: %s", state.reqURIWithQuery, err)
metadata, err = helper.WriteMetadata(state.reqURIWithQuery, "", state.targetHostName)
if err != nil {
log.Warnf("failed to build metadata for %s: %s", state.reqURIWithQuery, err)
}
}
// detect if source file has changed
if metadata.Checksum != helper.HashFile(rawImageAbs) {
log.Info("Source file has changed, re-encoding...")
metadata = helper.WriteMetadata(state.reqURIWithQuery, "", state.targetHostName)
metadata, err = helper.WriteMetadata(state.reqURIWithQuery, "", state.targetHostName)
if err != nil {
log.Warnf("failed to refresh metadata for %s: %s", state.reqURIWithQuery, err)
}
cleanProxyCache(path.Join(config.Config.ExhaustPath, state.targetHostName, metadata.Id))
}
}
Expand Down
61 changes: 44 additions & 17 deletions helper/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package helper

import (
"encoding/json"
"fmt"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -35,28 +36,43 @@ func getId(p string, subdir string) (id string, filePath string, santizedPath st
return id, filePath, santizedPath
}

func ReadMetadata(p, etag string, subdir string) config.MetaFile {
// try to read metadata, if we can't read, create one
func ReadMetadata(p, etag string, subdir string) (config.MetaFile, error) {
// Try to read metadata. If missing/corrupt, rebuild once.
var metadata config.MetaFile
var id, _, _ = getId(p, subdir)
metadataPath := path.Join(config.Config.MetadataPath, subdir, id+".json")

if buf, err := os.ReadFile(path.Join(config.Config.MetadataPath, subdir, id+".json")); err != nil {
// First time reading metadata, create one
WriteMetadata(p, etag, subdir)
return ReadMetadata(p, etag, subdir)
} else {
err = json.Unmarshal(buf, &metadata)
readAndUnmarshal := func() (config.MetaFile, error) {
buf, err := os.ReadFile(metadataPath)
if err != nil {
log.Warnf("unmarshal metadata error, possible corrupt file, re-building...: %s", err)
WriteMetadata(p, etag, subdir)
return ReadMetadata(p, etag, subdir)
return config.MetaFile{}, err
}
return metadata
if err := json.Unmarshal(buf, &metadata); err != nil {
return config.MetaFile{}, err
}
return metadata, nil
}

if data, err := readAndUnmarshal(); err == nil {
return data, nil
} else {
log.Warnf("read metadata failed, rebuilding: %s", err)
}

// Rebuild metadata once, then try reading again.
rebuilt, err := WriteMetadata(p, etag, subdir)
if err != nil {
return rebuilt, fmt.Errorf("failed to rebuild metadata at %s: %w", metadataPath, err)
}
data, err := readAndUnmarshal()
if err != nil {
return config.MetaFile{}, fmt.Errorf("failed to read metadata at %s after rebuild: %w", metadataPath, err)
}
return data, nil
}

func WriteMetadata(p, etag string, subdir string) config.MetaFile {
_ = os.MkdirAll(path.Join(config.Config.MetadataPath, subdir), 0755)
func WriteMetadata(p, etag string, subdir string) (config.MetaFile, error) {
metadataDir := path.Join(config.Config.MetadataPath, subdir)

var id, filepath, sant = getId(p, subdir)

Expand All @@ -78,9 +94,20 @@ func WriteMetadata(p, etag string, subdir string) config.MetaFile {
data.ImageMeta = imageMeta
}

buf, _ := json.Marshal(data)
_ = os.WriteFile(path.Join(config.Config.MetadataPath, subdir, data.Id+".json"), buf, 0644)
return data
if err := os.MkdirAll(metadataDir, 0755); err != nil {
return data, fmt.Errorf("create metadata dir %s: %w", metadataDir, err)
}

buf, err := json.Marshal(data)
if err != nil {
return data, fmt.Errorf("marshal metadata %s: %w", data.Id, err)
}

metadataPath := path.Join(config.Config.MetadataPath, subdir, data.Id+".json")
if err := os.WriteFile(metadataPath, buf, 0644); err != nil {
return data, fmt.Errorf("write metadata file %s: %w", metadataPath, err)
}
return data, nil
}

func getImageMeta(filePath string) (metadata config.ImageMeta) {
Expand Down
68 changes: 68 additions & 0 deletions helper/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package helper

import (
"net/url"
"os"
"path"
"path/filepath"
"strings"
"testing"
"webp_server_go/config"
)
Expand Down Expand Up @@ -41,3 +44,68 @@ func TestGetId(t *testing.T) {
}
})
}

func TestWriteAndReadMetadataSuccess(t *testing.T) {
tmpDir := t.TempDir()
imgDir := filepath.Join(tmpDir, "pics")
metadataDir := filepath.Join(tmpDir, "metadata")

config.Config.ImgPath = imgDir
config.Config.MetadataPath = metadataDir
config.Config.RemoteRawPath = filepath.Join(tmpDir, "remote-raw")

requireNoError := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

requireNoError(os.MkdirAll(imgDir, 0o755))
imagePath := filepath.Join(imgDir, "test.jpg")
requireNoError(os.WriteFile(imagePath, []byte("fake image bytes"), 0o600))

// Use URI-style local path to match getId() local branch behavior.
uri := "/test.jpg?width=200"

written, err := WriteMetadata(uri, "", config.LocalHostAlias)
requireNoError(err)
if written.Id == "" {
t.Fatalf("expected metadata id to be set")
}

read, err := ReadMetadata(uri, "", config.LocalHostAlias)
requireNoError(err)
if read.Id != written.Id {
t.Fatalf("expected same metadata id, got %s and %s", read.Id, written.Id)
}
}

func TestReadMetadataReturnsErrorWhenMetadataPathUnavailable(t *testing.T) {
tmpDir := t.TempDir()
imgDir := filepath.Join(tmpDir, "pics")
blockedPath := filepath.Join(tmpDir, "metadata-file")

config.Config.ImgPath = imgDir
config.Config.MetadataPath = blockedPath
config.Config.RemoteRawPath = filepath.Join(tmpDir, "remote-raw")

if err := os.MkdirAll(imgDir, 0o755); err != nil {
t.Fatalf("failed to create image dir: %v", err)
}
if err := os.WriteFile(filepath.Join(imgDir, "test.jpg"), []byte("fake image bytes"), 0o600); err != nil {
t.Fatalf("failed to write test image: %v", err)
}
// Make metadata base path a regular file so MkdirAll(path.Join(base, subdir)) fails.
if err := os.WriteFile(blockedPath, []byte("not a dir"), 0o600); err != nil {
t.Fatalf("failed to create blocked metadata path: %v", err)
}

_, err := ReadMetadata("/test.jpg?width=100", "", config.LocalHostAlias)
if err == nil {
t.Fatalf("expected error when metadata path is unavailable")
}
if !strings.Contains(strings.ToLower(err.Error()), "metadata") {
t.Fatalf("expected metadata-related error, got: %v", err)
}
}
Loading