Skip to content

Commit 9db1c3b

Browse files
committed
feat: 缩略图
1 parent 3188ba0 commit 9db1c3b

File tree

4 files changed

+125
-22
lines changed

4 files changed

+125
-22
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ go.work.sum
2626
/config.yaml
2727
/.idea
2828
/logs
29-
/static
29+
/static
30+
/thumbnail_cache

config.example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ oss:
3131
limit: 10 # 文件大小限制 单位: MB
3232
adminKey: "" # 管理员密钥
3333
quality: 75 # 图片质量(0-100)
34+
thumbnailDir: "./thumbnail_cache" # 缓存目录
35+
thumbnailQuality: 85 # 缩略图质量(0-100)
36+
thumbnailLongEdge: 640 # 缩略图长边像素
3437

3538
log:
3639
disableStacktrace: false # 是否禁用堆栈跟踪

internal/controllers/objectController/get.go

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package objectController
22

33
import (
44
"errors"
5+
"image"
56
"net/http"
67

78
"cube-go/internal/apiException"
@@ -19,6 +20,7 @@ type getFileListData struct {
1920
type getFileData struct {
2021
Bucket string `form:"bucket" binding:"required"`
2122
ObjectKey string `form:"object_key" binding:"required"`
23+
Thumbnail bool `form:"thumbnail"`
2224
}
2325

2426
// GetFileList 获取文件列表
@@ -58,27 +60,45 @@ func GetFile(c *gin.Context) {
5860
c.AbortWithStatus(http.StatusBadRequest)
5961
return
6062
}
63+
objectKey := objectService.CleanLocation(data.ObjectKey)
6164

62-
bucket, err := oss.Buckets.GetBucket(data.Bucket)
63-
if err != nil {
64-
c.AbortWithStatus(http.StatusNotFound)
65-
return
66-
}
65+
if data.Thumbnail {
66+
thumbnail, size, err := objectService.GetThumbnail(data.Bucket, objectKey)
67+
if errors.Is(err, oss.ErrResourceNotExists) || errors.Is(err, image.ErrFormat) {
68+
c.AbortWithStatus(http.StatusNotFound)
69+
return
70+
}
71+
if err != nil {
72+
c.AbortWithStatus(http.StatusInternalServerError)
73+
return
74+
}
75+
defer func() {
76+
_ = thumbnail.Close()
77+
}()
6778

68-
obj, content, err := bucket.GetObject(data.ObjectKey)
69-
if errors.Is(err, oss.ErrResourceNotExists) {
70-
c.AbortWithStatus(http.StatusNotFound)
71-
return
72-
}
73-
if err != nil {
74-
c.AbortWithStatus(http.StatusInternalServerError)
75-
return
76-
}
77-
defer func() {
78-
_ = obj.Close()
79-
}()
79+
c.DataFromReader(http.StatusOK, size, "image/jpeg", thumbnail, nil)
80+
} else {
81+
bucket, err := oss.Buckets.GetBucket(data.Bucket)
82+
if err != nil {
83+
c.AbortWithStatus(http.StatusNotFound)
84+
return
85+
}
8086

81-
c.DataFromReader(http.StatusOK, content.ContentLength, content.ContentType, obj, nil)
87+
obj, content, err := bucket.GetObject(objectKey)
88+
if errors.Is(err, oss.ErrResourceNotExists) {
89+
c.AbortWithStatus(http.StatusNotFound)
90+
return
91+
}
92+
if err != nil {
93+
c.AbortWithStatus(http.StatusInternalServerError)
94+
return
95+
}
96+
defer func() {
97+
_ = obj.Close()
98+
}()
99+
100+
c.DataFromReader(http.StatusOK, content.ContentLength, content.ContentType, obj, nil)
101+
}
82102
}
83103

84104
// GetBucketList 获取存储桶列表

internal/services/objectService/service.go

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import (
44
"bytes"
55
"image"
66
_ "image/gif" // 注册解码器
7-
_ "image/jpeg"
8-
_ "image/png"
7+
"image/jpeg"
8+
_ "image/png" // 注册解码器
99
"io"
10+
"os"
1011
"path"
12+
"path/filepath"
1113
"regexp"
1214
"strings"
1315
"sync"
1416

1517
"cube-go/pkg/config"
18+
"cube-go/pkg/oss"
1619
"github.com/chai2010/webp"
1720
"github.com/dustin/go-humanize"
1821
_ "golang.org/x/image/bmp" // 注册解码器
19-
_ "golang.org/x/image/tiff"
22+
"golang.org/x/image/draw"
23+
_ "golang.org/x/image/tiff" // 注册解码器
2024
_ "golang.org/x/image/webp"
2125
)
2226

@@ -66,3 +70,78 @@ func ConvertToWebP(reader io.Reader) (*bytes.Reader, error) {
6670
}
6771
return bytes.NewReader(buf.Bytes()), nil
6872
}
73+
74+
// GetThumbnail 获取缩略图
75+
func GetThumbnail(bucket string, objectKey string) (io.ReadCloser, int64, error) {
76+
filename := bucket + "-" + objectKey
77+
cachePath := filepath.Join(config.Config.GetString("oss.thumbnailDir"), filename+".jpg")
78+
79+
// 尝试从缓存中读取
80+
if stat, err := os.Stat(cachePath); err == nil {
81+
file, err := os.Open(cachePath)
82+
if err == nil {
83+
return file, stat.Size(), nil
84+
}
85+
}
86+
87+
// 从 OSS 获取源文件
88+
provider, err := oss.Buckets.GetBucket(bucket)
89+
if err != nil {
90+
return nil, 0, err
91+
}
92+
object, _, err := provider.GetObject(objectKey)
93+
if err != nil {
94+
return nil, 0, err
95+
}
96+
defer func() {
97+
_ = object.Close()
98+
}()
99+
100+
// 解码图片
101+
img, _, err := image.Decode(object)
102+
if err != nil {
103+
return nil, 0, err
104+
}
105+
finalImg := resizeIfNeeded(img, config.Config.GetInt("oss.thumbnailLongEdge"))
106+
107+
buf, _ := bufferPool.Get().(*bytes.Buffer)
108+
buf.Reset()
109+
defer bufferPool.Put(buf)
110+
111+
err = jpeg.Encode(buf, finalImg, &jpeg.Options{
112+
Quality: config.Config.GetInt("oss.thumbnailQuality"),
113+
})
114+
if err != nil {
115+
return nil, 0, err
116+
}
117+
118+
// 写入缓存文件
119+
if err := os.MkdirAll(filepath.Dir(cachePath), os.ModePerm); err == nil {
120+
_ = os.WriteFile(cachePath, buf.Bytes(), 0644)
121+
}
122+
123+
return io.NopCloser(bytes.NewReader(buf.Bytes())), int64(buf.Len()), nil
124+
}
125+
126+
func resizeIfNeeded(img image.Image, targetLongSide int) image.Image {
127+
b := img.Bounds()
128+
w, h := b.Dx(), b.Dy()
129+
longSide, shortSide := max(w, h), min(w, h)
130+
131+
if longSide <= targetLongSide {
132+
return img
133+
}
134+
scale := float64(targetLongSide) / float64(longSide)
135+
targetShort := int(float64(shortSide) * scale)
136+
137+
var tw, th int
138+
if w > h {
139+
tw, th = targetLongSide, targetShort
140+
} else {
141+
tw, th = targetShort, targetLongSide
142+
}
143+
144+
dst := image.NewRGBA(image.Rect(0, 0, tw, th))
145+
draw.BiLinear.Scale(dst, dst.Rect, img, b, draw.Over, nil)
146+
return dst
147+
}

0 commit comments

Comments
 (0)