Add resampled image retrieval functionality and update image URLs

This commit is contained in:
2025-06-23 21:19:42 +08:00
parent dcd23054b2
commit be067cc21a
6 changed files with 117 additions and 5 deletions

View File

@@ -24,7 +24,7 @@ export default function ResourceCard({ resource }: { resource: Resource }) {
{resource.image != null && ( {resource.image != null && (
<figure> <figure>
<img <img
src={network.getImageUrl(resource.image.id)} src={network.getResampledImageUrl(resource.image.id)}
alt="cover" alt="cover"
style={{ style={{
width: "100%", width: "100%",

View File

@@ -492,6 +492,10 @@ class Network {
return `${this.apiBaseUrl}/image/${id}`; return `${this.apiBaseUrl}/image/${id}`;
} }
getResampledImageUrl(id: number): string {
return `${this.apiBaseUrl}/image/resampled/${id}`;
}
async createResource( async createResource(
params: CreateResourceParams, params: CreateResourceParams,
): Promise<Response<number>> { ): Promise<Response<number>> {

9
go.mod
View File

@@ -15,7 +15,10 @@ require (
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
) )
require github.com/go-sql-driver/mysql v1.7.0 // indirect require (
github.com/disintegration/imaging v1.6.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
)
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -47,8 +50,8 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.61.0 // indirect github.com/valyala/fasthttp v1.61.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/image v0.27.0 golang.org/x/image v0.28.0
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/text v0.26.0 // indirect
) )

7
go.sum
View File

@@ -4,6 +4,8 @@ github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
@@ -78,8 +80,11 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
@@ -90,6 +95,8 @@ golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -71,10 +71,30 @@ func handleDeleteImage(c fiber.Ctx) error {
}) })
} }
func handleGetResampledImage(c fiber.Ctx) error {
idStr := c.Params("id")
if idStr == "" {
return model.NewRequestError("Image ID is required")
}
id, err := strconv.Atoi(idStr)
if err != nil {
return model.NewRequestError("Invalid image ID")
}
image, err := service.GetResampledImage(uint(id))
if err != nil {
return err
}
contentType := http.DetectContentType(image)
c.Set("Content-Type", contentType)
c.Set("Cache-Control", "public, max-age=31536000")
return c.Send(image)
}
func AddImageRoutes(api fiber.Router) { func AddImageRoutes(api fiber.Router) {
image := api.Group("/image") image := api.Group("/image")
{ {
image.Put("/", handleUploadImage) image.Put("/", handleUploadImage)
image.Get("/resampled/:id", handleGetResampledImage)
image.Get("/:id", handleGetImage) image.Get("/:id", handleGetImage)
image.Delete("/:id", handleDeleteImage) image.Delete("/:id", handleDeleteImage)
} }

View File

@@ -3,12 +3,14 @@ package service
import ( import (
"bytes" "bytes"
"errors" "errors"
"github.com/disintegration/imaging"
"image" "image"
"net/http" "net/http"
"nysoure/server/dao" "nysoure/server/dao"
"nysoure/server/model" "nysoure/server/model"
"nysoure/server/utils" "nysoure/server/utils"
"os" "os"
"strconv"
"time" "time"
"github.com/gofiber/fiber/v3/log" "github.com/gofiber/fiber/v3/log"
@@ -24,6 +26,10 @@ import (
"github.com/chai2010/webp" "github.com/chai2010/webp"
) )
const (
resampledMaxPixels = 1280 * 720
)
func init() { func init() {
// Start a goroutine to delete unused images every hour // Start a goroutine to delete unused images every hour
go func() { go func() {
@@ -152,11 +158,83 @@ func deleteImage(id uint) error {
} }
imageDir := utils.GetStoragePath() + "/images/" imageDir := utils.GetStoragePath() + "/images/"
_ = os.Remove(imageDir + i.FileName) _ = os.Remove(imageDir + i.FileName)
resampledDir := utils.GetStoragePath() + "/resampled/"
_ = os.Remove(resampledDir + strconv.Itoa(int(i.ID)) + ".webp")
if err := dao.DeleteImage(id); err != nil { if err := dao.DeleteImage(id); err != nil {
return err return err
} }
return nil return nil
} }
func GetResampledImage(id uint) ([]byte, error) {
i, err := dao.GetImageByID(id)
if err != nil {
return nil, err
}
data, err := getOrCreateResampledImage(i)
if err != nil {
log.Error("Error getting or creating resampled image:", err)
return nil, model.NewInternalServerError("Error processing image")
}
return data, nil
}
func getOrCreateResampledImage(i model.Image) ([]byte, error) {
baseDir := utils.GetStoragePath() + "/resampled/"
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
if err := os.MkdirAll(baseDir, 0755); err != nil {
return nil, err
}
}
resampledFilepath := baseDir + strconv.Itoa(int(i.ID)) + ".webp"
if _, err := os.Stat(resampledFilepath); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
return os.ReadFile(resampledFilepath)
}
originalFilepath := utils.GetStoragePath() + "/images/" + i.FileName
if _, err := os.Stat(originalFilepath); os.IsNotExist(err) {
return nil, model.NewNotFoundError("Original image not found")
}
imgData, err := os.ReadFile(originalFilepath)
if err != nil {
return nil, errors.New("failed to read original image file")
}
if i.Width*i.Height <= resampledMaxPixels {
return imgData, nil
}
log.Info("Resampling image", "id", i.ID, "original size", i.Width, "x", i.Height)
img, _, err := image.Decode(bytes.NewReader(imgData))
if err != nil {
return nil, errors.New("failed to decode original image data")
}
pixels := img.Bounds().Dx() * img.Bounds().Dy()
if pixels <= resampledMaxPixels {
return imgData, nil // No need to resample if the image is small enough
}
scale := float64(resampledMaxPixels) / float64(pixels)
dstWidth := int(float64(img.Bounds().Dx()) * scale)
dstHeight := int(float64(img.Bounds().Dy()) * scale)
dstImg := imaging.Resize(img, dstWidth, dstHeight, imaging.Lanczos)
buf := new(bytes.Buffer)
if err := webp.Encode(buf, dstImg, &webp.Options{Quality: 80}); err != nil {
return nil, errors.New("failed to encode resampled image data to webp format")
}
if err := os.WriteFile(resampledFilepath, buf.Bytes(), 0644); err != nil {
return nil, errors.New("failed to save resampled image file")
}
return buf.Bytes(), nil
}