mirror of
https://github.com/woodchen-ink/webp_server_go.git
synced 2025-07-18 13:42:02 +08:00
260 lines
8.2 KiB
Go
260 lines
8.2 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"net/url"
|
||
"path"
|
||
"regexp"
|
||
"slices"
|
||
"strconv"
|
||
"strings"
|
||
"webp_server_go/config"
|
||
"webp_server_go/encoder"
|
||
"webp_server_go/helper"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
log "github.com/sirupsen/logrus"
|
||
)
|
||
|
||
func Convert(c *fiber.Ctx) error {
|
||
// this function need to do:
|
||
// 1. get request path, query string
|
||
// 2. generate rawImagePath, could be local path or remote url(possible with query string)
|
||
// 3. pass it to encoder, get the result, send it back
|
||
|
||
// normal http request will start with /
|
||
// 检查路径是否以 "/" 开头
|
||
if !strings.HasPrefix(c.Path(), "/") {
|
||
return c.SendStatus(http.StatusBadRequest)
|
||
}
|
||
|
||
// 处理根路径请求
|
||
if c.Path() == "/" {
|
||
return c.SendString("Welcome to WebP Server")
|
||
}
|
||
|
||
var (
|
||
reqHostname = c.Hostname()
|
||
reqHost = c.Protocol() + "://" + reqHostname // http://www.example.com:8000
|
||
reqHeader = &c.Request().Header
|
||
|
||
reqURIRaw, _ = url.QueryUnescape(c.Path()) // /mypic/123.jpg
|
||
reqURIwithQueryRaw, _ = url.QueryUnescape(c.OriginalURL()) // /mypic/123.jpg?someother=200&somebugs=200
|
||
reqURI = path.Clean(reqURIRaw) // delete ../ in reqURI to mitigate directory traversal
|
||
reqURIwithQuery = path.Clean(reqURIwithQueryRaw) // Sometimes reqURIwithQuery can be https://example.tld/mypic/123.jpg?someother=200&somebugs=200, we need to extract it
|
||
|
||
filename = path.Base(reqURI)
|
||
realRemoteAddr = ""
|
||
targetHostName = config.LocalHostAlias
|
||
targetHost = config.Config.ImgPath
|
||
proxyMode = config.ProxyMode
|
||
mapMode = false
|
||
|
||
width, _ = strconv.Atoi(c.Query("width")) // Extra Params
|
||
height, _ = strconv.Atoi(c.Query("height")) // Extra Params
|
||
maxHeight, _ = strconv.Atoi(c.Query("max_height")) // Extra Params
|
||
maxWidth, _ = strconv.Atoi(c.Query("max_width")) // Extra Params
|
||
extraParams = config.ExtraParams{
|
||
Width: width,
|
||
Height: height,
|
||
MaxWidth: maxWidth,
|
||
MaxHeight: maxHeight,
|
||
}
|
||
)
|
||
|
||
log.Debugf("Incoming connection from %s %s %s", c.IP(), reqHostname, reqURIwithQuery)
|
||
|
||
// 非图片清况下302到源文件
|
||
//
|
||
//
|
||
//
|
||
|
||
if !isImageFile(filename) {
|
||
log.Infof("Non-image file requested: %s", reqURI)
|
||
var redirectURL string
|
||
|
||
// 检查是否存在匹配的 IMG_MAP
|
||
for prefix, target := range config.Config.ImageMap {
|
||
if strings.HasPrefix(reqURI, prefix) {
|
||
// 检查目标是否为远程资源
|
||
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
|
||
// 远程资源,构造重定向 URL
|
||
redirectURL = target + strings.TrimPrefix(reqURI, prefix)
|
||
} else {
|
||
// 本地资源,按原逻辑处理
|
||
return c.SendFile(path.Join(target, strings.TrimPrefix(reqURI, prefix)))
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果没有找到匹配的 IMG_MAP,或者是本地资源,使用默认的处理方式
|
||
if redirectURL == "" {
|
||
if proxyMode {
|
||
redirectURL = realRemoteAddr
|
||
} else {
|
||
// 本地资源,按原逻辑处理
|
||
localPath := path.Join(config.Config.ImgPath, reqURI)
|
||
if helper.FileExists(localPath) {
|
||
return c.SendFile(localPath)
|
||
} else {
|
||
return c.SendStatus(fiber.StatusNotFound)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 只有在确定需要重定向时才执行重定向
|
||
if redirectURL != "" {
|
||
log.Infof("Redirecting to: %s", redirectURL)
|
||
return c.Redirect(redirectURL, fiber.StatusFound)
|
||
}
|
||
}
|
||
|
||
//
|
||
//
|
||
|
||
if !helper.CheckAllowedType(filename) {
|
||
msg := "File extension not allowed! " + filename
|
||
log.Warn(msg)
|
||
c.Status(http.StatusBadRequest)
|
||
_ = c.Send([]byte(msg))
|
||
return nil
|
||
}
|
||
|
||
// Rewrite the target backend if a mapping rule matches the hostname
|
||
if hostMap, hostMapFound := config.Config.ImageMap[reqHost]; hostMapFound {
|
||
log.Debugf("Found host mapping %s -> %s", reqHostname, hostMap)
|
||
targetHostUrl, _ := url.Parse(hostMap)
|
||
targetHostName = targetHostUrl.Host
|
||
targetHost = targetHostUrl.Scheme + "://" + targetHostUrl.Host
|
||
proxyMode = true
|
||
} else {
|
||
// There's not matching host mapping, now check for any URI map that apply
|
||
httpRegexpMatcher := regexp.MustCompile(config.HttpRegexp)
|
||
for uriMap, uriMapTarget := range config.Config.ImageMap {
|
||
if strings.HasPrefix(reqURI, uriMap) {
|
||
log.Debugf("Found URI mapping %s -> %s", uriMap, uriMapTarget)
|
||
mapMode = true
|
||
|
||
// if uriMapTarget we use the proxy mode to fetch the remote
|
||
if httpRegexpMatcher.Match([]byte(uriMapTarget)) {
|
||
targetHostUrl, _ := url.Parse(uriMapTarget)
|
||
targetHostName = targetHostUrl.Host
|
||
targetHost = targetHostUrl.Scheme + "://" + targetHostUrl.Host
|
||
reqURI = strings.Replace(reqURI, uriMap, targetHostUrl.Path, 1)
|
||
reqURIwithQuery = strings.Replace(reqURIwithQuery, uriMap, targetHostUrl.Path, 1)
|
||
proxyMode = true
|
||
} else {
|
||
reqURI = strings.Replace(reqURI, uriMap, uriMapTarget, 1)
|
||
reqURIwithQuery = strings.Replace(reqURIwithQuery, uriMap, uriMapTarget, 1)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
if proxyMode {
|
||
|
||
if !mapMode {
|
||
// Don't deal with the encoding to avoid upstream compatibilities
|
||
reqURI = c.Path()
|
||
reqURIwithQuery = c.OriginalURL()
|
||
}
|
||
|
||
log.Tracef("reqURIwithQuery is %s", reqURIwithQuery)
|
||
|
||
// Replace host in the URL
|
||
// realRemoteAddr = strings.Replace(reqURIwithQuery, reqHost, targetHost, 1)
|
||
realRemoteAddr = targetHost + reqURIwithQuery
|
||
log.Debugf("realRemoteAddr is %s", realRemoteAddr)
|
||
}
|
||
|
||
var rawImageAbs string
|
||
var metadata = config.MetaFile{}
|
||
if proxyMode {
|
||
// this is proxyMode, we'll have to use this url to download and save it to local path, which also gives us rawImageAbs
|
||
// https://test.webp.sh/mypic/123.jpg?someother=200&somebugs=200
|
||
|
||
metadata = fetchRemoteImg(realRemoteAddr, targetHostName)
|
||
rawImageAbs = path.Join(config.Config.RemoteRawPath, targetHostName, metadata.Id)
|
||
} else {
|
||
// not proxyMode, we'll use local path
|
||
metadata = helper.ReadMetadata(reqURIwithQuery, "", targetHostName)
|
||
if !mapMode {
|
||
// by default images are hosted in ImgPath
|
||
rawImageAbs = path.Join(config.Config.ImgPath, reqURI)
|
||
} else {
|
||
rawImageAbs = reqURI
|
||
}
|
||
// detect if source file has changed
|
||
if metadata.Checksum != helper.HashFile(rawImageAbs) {
|
||
log.Info("Source file has changed, re-encoding...")
|
||
helper.WriteMetadata(reqURIwithQuery, "", targetHostName)
|
||
cleanProxyCache(path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id))
|
||
}
|
||
}
|
||
|
||
supportedFormats := helper.GuessSupportedFormat(reqHeader)
|
||
// resize itself and return if only raw(original format) is supported
|
||
if supportedFormats["raw"] == true &&
|
||
supportedFormats["webp"] == false &&
|
||
supportedFormats["avif"] == false &&
|
||
supportedFormats["jxl"] == false {
|
||
dest := path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id)
|
||
if !helper.ImageExists(dest) {
|
||
encoder.ResizeItself(rawImageAbs, dest, extraParams)
|
||
}
|
||
return c.SendFile(dest)
|
||
}
|
||
|
||
// Check the original image for existence,
|
||
if !helper.ImageExists(rawImageAbs) {
|
||
helper.DeleteMetadata(reqURIwithQuery, targetHostName)
|
||
msg := "Image not found!"
|
||
_ = c.Send([]byte(msg))
|
||
log.Warn(msg)
|
||
_ = c.SendStatus(404)
|
||
return nil
|
||
}
|
||
|
||
avifAbs, webpAbs, jxlAbs := helper.GenOptimizedAbsPath(metadata, targetHostName)
|
||
// Do the convertion based on supported formats and config
|
||
encoder.ConvertFilter(rawImageAbs, jxlAbs, avifAbs, webpAbs, extraParams, supportedFormats, nil)
|
||
|
||
var availableFiles = []string{rawImageAbs}
|
||
if supportedFormats["avif"] {
|
||
availableFiles = append(availableFiles, avifAbs)
|
||
}
|
||
if supportedFormats["webp"] {
|
||
availableFiles = append(availableFiles, webpAbs)
|
||
}
|
||
if supportedFormats["jxl"] {
|
||
availableFiles = append(availableFiles, jxlAbs)
|
||
}
|
||
|
||
finalFilename := helper.FindSmallestFiles(availableFiles)
|
||
contentType := helper.GetFileContentType(finalFilename)
|
||
c.Set("Content-Type", contentType)
|
||
|
||
c.Set("X-Compression-Rate", helper.GetCompressionRate(rawImageAbs, finalFilename))
|
||
return c.SendFile(finalFilename)
|
||
}
|
||
|
||
// 新增:检查文件是否为图片的辅助函数
|
||
func isImageFile(filename string) bool {
|
||
ext := strings.ToLower(path.Ext(filename))
|
||
if ext == "" {
|
||
return false
|
||
}
|
||
ext = ext[1:] // 移除开头的点
|
||
|
||
allowedTypes := config.Config.AllowedTypes
|
||
if len(allowedTypes) == 1 && allowedTypes[0] == "*" {
|
||
// 如果允许所有类型,则使用默认的图片类型列表
|
||
allowedTypes = config.NewWebPConfig().AllowedTypes
|
||
}
|
||
|
||
return slices.Contains(allowedTypes, ext)
|
||
}
|