Skip to content

Commit 689d4b9

Browse files
authored
Merge pull request #330 from ThiagoBauken/fix/sendimage-temp-leak
fix: stop leaking a temp file on every image send
2 parents 7594594 + c3ade4f commit 689d4b9

2 files changed

Lines changed: 117 additions & 18 deletions

File tree

handlers.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,21 @@ func (s *server) SendAudio() http.HandlerFunc {
12091209
}
12101210
}
12111211

1212+
// jpegThumbnail resizes img to fit within width x height (preserving aspect
1213+
// ratio) and returns it JPEG-encoded. It encodes in memory, so there is no temp
1214+
// file to leak.
1215+
func jpegThumbnail(img image.Image, width, height uint) ([]byte, error) {
1216+
if img == nil {
1217+
return nil, errors.New("cannot create thumbnail from a nil image")
1218+
}
1219+
thumb := resize.Thumbnail(width, height, img, resize.Lanczos3)
1220+
var buf bytes.Buffer
1221+
if err := jpeg.Encode(&buf, thumb, nil); err != nil {
1222+
return nil, err
1223+
}
1224+
return buf.Bytes(), nil
1225+
}
1226+
12121227
// Sends an Image message
12131228
func (s *server) SendImage() http.HandlerFunc {
12141229

@@ -1312,25 +1327,11 @@ func (s *server) SendImage() http.HandlerFunc {
13121327
return
13131328
}
13141329

1315-
// resize to width 72 using Lanczos resampling and preserve aspect ratio
1316-
m := resize.Thumbnail(72, 72, img, resize.Lanczos3)
1317-
1318-
tmpFile, err := os.CreateTemp("", "resized-*.jpg")
1319-
if err != nil {
1320-
s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create temp file for thumbnail: %v", err)))
1321-
return
1322-
}
1323-
defer tmpFile.Close()
1324-
1325-
// write new image to file
1326-
if err := jpeg.Encode(tmpFile, m, nil); err != nil {
1327-
s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to encode jpeg: %v", err)))
1328-
return
1329-
}
1330-
1331-
thumbnailBytes, err = os.ReadFile(tmpFile.Name())
1330+
// resize to 72x72 (preserving aspect ratio) and encode the thumbnail in
1331+
// memory — no temp file, so there is nothing to leak.
1332+
thumbnailBytes, err = jpegThumbnail(img, 72, 72)
13321333
if err != nil {
1333-
s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to read %s: %v", tmpFile.Name(), err)))
1334+
s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to encode jpeg thumbnail: %v", err)))
13341335
return
13351336
}
13361337

jpeg_thumbnail_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"image"
6+
"image/color"
7+
"image/jpeg"
8+
_ "image/png"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/nfnt/resize"
14+
)
15+
16+
// TestJpegThumbnail covers the in-memory thumbnail encoding that replaced the
17+
// leaking temp-file path in SendImage: it must return decodable JPEG bytes
18+
// bounded by the requested size, preserving aspect ratio.
19+
func TestJpegThumbnail(t *testing.T) {
20+
// A 200x100 source image (2:1 aspect).
21+
src := image.NewRGBA(image.Rect(0, 0, 200, 100))
22+
for x := 0; x < 200; x++ {
23+
for y := 0; y < 100; y++ {
24+
src.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 0, A: 255})
25+
}
26+
}
27+
28+
out, err := jpegThumbnail(src, 72, 72)
29+
if err != nil {
30+
t.Fatalf("jpegThumbnail: %v", err)
31+
}
32+
if len(out) == 0 {
33+
t.Fatal("jpegThumbnail returned no bytes")
34+
}
35+
36+
cfg, format, err := image.DecodeConfig(bytes.NewReader(out))
37+
if err != nil {
38+
t.Fatalf("output is not a decodable image: %v", err)
39+
}
40+
if format != "jpeg" {
41+
t.Errorf("format = %q; want jpeg", format)
42+
}
43+
if cfg.Width == 0 || cfg.Height == 0 {
44+
t.Errorf("thumbnail has a zero dimension: %dx%d", cfg.Width, cfg.Height)
45+
}
46+
if cfg.Width > 72 || cfg.Height > 72 {
47+
t.Errorf("thumbnail %dx%d exceeds the 72x72 bound", cfg.Width, cfg.Height)
48+
}
49+
}
50+
51+
// TestJpegThumbnailNil verifies the nil-image guard returns an error instead of
52+
// panicking (resize.Thumbnail dereferences the image's bounds).
53+
func TestJpegThumbnailNil(t *testing.T) {
54+
if _, err := jpegThumbnail(nil, 72, 72); err == nil {
55+
t.Error("expected an error for a nil image, got nil")
56+
}
57+
}
58+
59+
// TestJpegThumbnailRealImage runs the fix end-to-end against a real image file
60+
// shipped with the project. It decodes the image exactly like SendImage does
61+
// (image.Decode), produces the thumbnail, and asserts the output is byte-for-byte
62+
// identical to the previous resize+encode — so removing the temp file changed
63+
// nothing but the mechanism — and is a valid, bounded JPEG.
64+
func TestJpegThumbnailRealImage(t *testing.T) {
65+
data, err := os.ReadFile(filepath.Join("static", "images", "background_image.png"))
66+
if err != nil {
67+
t.Skipf("real image not available: %v", err)
68+
}
69+
70+
img, format, err := image.Decode(bytes.NewReader(data))
71+
if err != nil {
72+
t.Fatalf("decode real image: %v", err)
73+
}
74+
t.Logf("decoded a real %s image: %dx%d (%T)", format, img.Bounds().Dx(), img.Bounds().Dy(), img)
75+
76+
got, err := jpegThumbnail(img, 72, 72)
77+
if err != nil {
78+
t.Fatalf("jpegThumbnail on real image: %v", err)
79+
}
80+
81+
// Reference: the exact resize + JPEG encode the old temp-file path performed.
82+
var ref bytes.Buffer
83+
if err := jpeg.Encode(&ref, resize.Thumbnail(72, 72, img, resize.Lanczos3), nil); err != nil {
84+
t.Fatalf("reference encode: %v", err)
85+
}
86+
if !bytes.Equal(got, ref.Bytes()) {
87+
t.Errorf("thumbnail differs from the resize+encode reference (%d vs %d bytes)", len(got), ref.Len())
88+
}
89+
90+
cfg, f, err := image.DecodeConfig(bytes.NewReader(got))
91+
if err != nil {
92+
t.Fatalf("thumbnail is not a decodable image: %v", err)
93+
}
94+
t.Logf("thumbnail produced: %s %dx%d, %d bytes", f, cfg.Width, cfg.Height, len(got))
95+
if f != "jpeg" || cfg.Width == 0 || cfg.Width > 72 || cfg.Height > 72 {
96+
t.Errorf("unexpected thumbnail: format=%s size=%dx%d", f, cfg.Width, cfg.Height)
97+
}
98+
}

0 commit comments

Comments
 (0)