refactor(thumbnails): thread context.Context through the preprocessor Convert interface

The Tika preview extractor does a network call; it was using context.Background().
Pass the request context from the gRPC handler through Convert so the unpack call
respects cancellation and deadlines. Non-I/O decoders ignore the context.
This commit is contained in:
Dominik Schmidt committed 2026-08-19 18:45:11 +02:00
1 parent c286f771c9
commit 75893c19ee
8 files changed
+40 -34

No files matched your search

@@ -4,6 +4,7 @@ import (
"archive/zip"
"bufio"
"bytes"
"context"
"encoding/base64"
"encoding/json"
"image"
@@ -26,14 +27,14 @@ import (
// FileConverter is the interface for the file converter
type FileConverter interface {
Convert(r io.Reader) (any, error)
Convert(ctx context.Context, r io.Reader) (any, error)
}
// GifDecoder is a converter for the gif file
type GifDecoder struct{}
// Convert reads the gif file and returns the thumbnail image
func (i GifDecoder) Convert(r io.Reader) (any, error) {
func (i GifDecoder) Convert(_ context.Context, r io.Reader) (any, error) {
img, err := gif.DecodeAll(r)
if err != nil {
return nil, errors.Wrap(err, `could not decode the image`)
@@ -45,7 +46,7 @@ func (i GifDecoder) Convert(r io.Reader) (any, error) {
type GgsDecoder struct{ thumbnailpath string }
// Convert reads the ggs file and returns the thumbnail image
func (g GgsDecoder) Convert(r io.Reader) (any, error) {
func (g GgsDecoder) Convert(ctx context.Context, r io.Reader) (any, error) {
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
@@ -65,7 +66,7 @@ func (g GgsDecoder) Convert(r io.Reader) (any, error) {
if converter == nil {
return nil, thumbnailerErrors.ErrNoConverterForExtractedImageFromGgsFile
}
img, err := converter.Convert(thumbnail)
img, err := converter.Convert(ctx, thumbnail)
if err != nil {
return nil, errors.Wrap(err, `could not decode the image`)
}
@@ -79,7 +80,7 @@ func (g GgsDecoder) Convert(r io.Reader) (any, error) {
type AudioDecoder struct{}
// Convert reads the audio file and extracts the thumbnail image from the id3 tag
func (i AudioDecoder) Convert(r io.Reader) (any, error) {
func (i AudioDecoder) Convert(ctx context.Context, r io.Reader) (any, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, err
@@ -99,7 +100,7 @@ func (i AudioDecoder) Convert(r io.Reader) (any, error) {
return nil, thumbnailerErrors.ErrNoConverterForExtractedImageFromAudioFile
}
return converter.Convert(bytes.NewReader(picture.Data))
return converter.Convert(ctx, bytes.NewReader(picture.Data))
}
// TxtToImageConverter is a converter for the text file
@@ -108,7 +109,7 @@ type TxtToImageConverter struct {
}
// Convert reads the text file and renders it into a thumbnail image
func (t TxtToImageConverter) Convert(r io.Reader) (any, error) {
func (t TxtToImageConverter) Convert(_ context.Context, r io.Reader) (any, error) {
img := image.NewRGBA(image.Rect(0, 0, 640, 480))
imgBounds := img.Bounds()
@@ -203,7 +204,7 @@ type GGPStruct struct {
type GgpDecoder struct{}
// Convert reads the ggp file and returns the first thumbnail image
func (j GgpDecoder) Convert(r io.Reader) (any, error) {
func (j GgpDecoder) Convert(_ context.Context, r io.Reader) (any, error) {
ggp := &GGPStruct{}
err := json.NewDecoder(r).Decode(ggp)
if err != nil {
@@ -3,6 +3,7 @@
package preprocessor
import (
"context"
"io"
"github.com/kovidgoyal/imaging"
@@ -13,7 +14,7 @@ import (
type ImageDecoder struct{}
// Convert reads the image file and returns the thumbnail image
func (i ImageDecoder) Convert(r io.Reader) (any, error) {
func (i ImageDecoder) Convert(_ context.Context, r io.Reader) (any, error) {
img, err := imaging.Decode(r, imaging.AutoOrientation(true))
if err != nil {
return nil, errors.Wrap(err, `could not decode the image`)
@@ -2,6 +2,7 @@ package preprocessor
import (
"bytes"
"context"
"io"
"os"
"testing"
@@ -32,14 +33,14 @@ var _ = Describe("ImageDecoder", func() {
It("should decode an image", func() {
decoder := ImageDecoder{}
img, err := decoder.Convert(fileReader)
img, err := decoder.Convert(context.TODO(), fileReader)
Expect(err).ToNot(HaveOccurred())
Expect(img).ToNot(BeNil())
})
It("should return an error if the image is invalid", func() {
decoder := ImageDecoder{}
img, err := decoder.Convert(bytes.NewReader([]byte("not an image")))
img, err := decoder.Convert(context.TODO(), bytes.NewReader([]byte("not an image")))
Expect(err).To(HaveOccurred())
Expect(img).To(BeNil())
})
@@ -57,14 +58,14 @@ var _ = Describe("ImageDecoder", func() {
It("should decode a gif", func() {
decoder := GifDecoder{}
img, err := decoder.Convert(fileReader)
img, err := decoder.Convert(context.TODO(), fileReader)
Expect(err).ToNot(HaveOccurred())
Expect(img).ToNot(BeNil())
})
It("should return an error if the gif is invalid", func() {
decoder := GifDecoder{}
img, err := decoder.Convert(bytes.NewReader([]byte("not a gif")))
img, err := decoder.Convert(context.TODO(), bytes.NewReader([]byte("not a gif")))
Expect(err).To(HaveOccurred())
Expect(img).To(BeNil())
})
@@ -82,14 +83,14 @@ var _ = Describe("ImageDecoder", func() {
It("should decode a ggs", func() {
decoder := GgsDecoder{"_slide0/geogebra_thumbnail.png"}
img, err := decoder.Convert(fileReader)
img, err := decoder.Convert(context.TODO(), fileReader)
Expect(err).ToNot(HaveOccurred())
Expect(img).ToNot(BeNil())
})
It("should return an error if the ggs is invalid", func() {
decoder := GgsDecoder{"_slide0/geogebra_thumbnail.png"}
img, err := decoder.Convert(bytes.NewReader([]byte("not a ggs")))
img, err := decoder.Convert(context.TODO(), bytes.NewReader([]byte("not a ggs")))
Expect(err).To(HaveOccurred())
Expect(img).To(BeNil())
})
@@ -104,7 +105,7 @@ var _ = Describe("ImageDecoder", func() {
}
fileReader = bytes.NewReader(fileContent)
decoder := AudioDecoder{}
img, err := decoder.Convert(fileReader)
img, err := decoder.Convert(context.TODO(), fileReader)
Expect(err).ToNot(HaveOccurred())
Expect(img).ToNot(BeNil())
})
@@ -115,13 +116,13 @@ var _ = Describe("ImageDecoder", func() {
}
fileReader = bytes.NewReader(fileContent)
decoder := AudioDecoder{}
img, err := decoder.Convert(fileReader)
img, err := decoder.Convert(context.TODO(), fileReader)
Expect(err).To(HaveOccurred())
Expect(img).To(BeNil())
})
It("should return an error if the audio is invalid", func() {
decoder := AudioDecoder{}
img, err := decoder.Convert(bytes.NewReader([]byte("not an audio")))
img, err := decoder.Convert(context.TODO(), bytes.NewReader([]byte("not an audio")))
Expect(err).To(HaveOccurred())
Expect(img).To(BeNil())
})
@@ -145,7 +146,7 @@ var _ = Describe("ImageDecoder", func() {
}
})
It("should decode a text", func() {
img, err := decoder.Convert(bytes.NewReader([]byte("This is a test text")))
img, err := decoder.Convert(context.TODO(), bytes.NewReader([]byte("This is a test text")))
Expect(err).ToNot(HaveOccurred())
Expect(img).ToNot(BeNil())
})
@@ -156,7 +157,7 @@ var _ = Describe("ImageDecoder", func() {
DefaultFont: "/some/unknown/font.otf",
},
}
img, err := decoder.Convert(bytes.NewReader([]byte("This is a test text")))
img, err := decoder.Convert(context.TODO(), bytes.NewReader([]byte("This is a test text")))
Expect(err).To(HaveOccurred())
Expect(img).To(BeNil())
})
@@ -3,6 +3,7 @@
package preprocessor
import (
"context"
"io"
"github.com/davidbyttow/govips/v2/vips"
@@ -14,7 +15,7 @@ func init() {
type ImageDecoder struct{}
func (v ImageDecoder) Convert(r io.Reader) (interface{}, error) {
func (v ImageDecoder) Convert(_ context.Context, r io.Reader) (interface{}, error) {
img, err := vips.NewImageFromReader(r)
return img, err
}
+3 -3
View File
@@ -27,16 +27,16 @@ type TikaDecoder struct {
filename string
}
func (d TikaDecoder) Convert(r io.Reader) (any, error) {
func (d TikaDecoder) Convert(ctx context.Context, r io.Reader) (any, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
preview, err := tikaExtractPreview(context.Background(), d.tikaURL, d.filename, data)
preview, err := tikaExtractPreview(ctx, d.tikaURL, d.filename, data)
if err != nil {
return nil, err
}
return ForType("image/jpeg", nil).Convert(bytes.NewReader(preview))
return ForType("image/jpeg", nil).Convert(ctx, bytes.NewReader(preview))
}
var tikaHTTPClient = &http.Client{Timeout: 30 * time.Second}
@@ -3,6 +3,7 @@ package preprocessor
import (
"archive/zip"
"bytes"
"context"
"image"
"image/jpeg"
"io"
@@ -62,7 +63,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
img, err := TikaDecoder{tikaURL: srv.URL}.Convert(bytes.NewReader([]byte("raw")))
img, err := TikaDecoder{tikaURL: srv.URL}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).ToNot(HaveOccurred())
// default (imaging) build decodes to an image.Image
bounds := img.(image.Image).Bounds()
@@ -76,7 +77,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(bytes.NewReader([]byte("raw")))
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).To(MatchError(thumbnailerErrors.ErrNoImageFromRawFile))
})
@@ -86,7 +87,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(bytes.NewReader([]byte("raw")))
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).To(HaveOccurred())
})
@@ -98,7 +99,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
_, err := TikaDecoder{tikaURL: srv.URL, filename: "my photo.nef"}.Convert(bytes.NewReader([]byte("raw")))
_, err := TikaDecoder{tikaURL: srv.URL, filename: "my photo.nef"}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).ToNot(HaveOccurred())
Expect(gotCD).To(Equal(`attachment; filename="my photo.nef"`))
})
@@ -112,7 +113,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
img, err := TikaDecoder{tikaURL: srv.URL}.Convert(bytes.NewReader([]byte("raw")))
img, err := TikaDecoder{tikaURL: srv.URL}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).ToNot(HaveOccurred())
Expect(img.(image.Image).Bounds().Dx()).To(Equal(16))
})
@@ -123,7 +124,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(bytes.NewReader([]byte("raw")))
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).To(MatchError(thumbnailerErrors.ErrNoImageFromRawFile))
})
@@ -133,7 +134,7 @@ var _ = Describe("TikaDecoder", func() {
}))
defer srv.Close()
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(bytes.NewReader([]byte("raw")))
_, err := TikaDecoder{tikaURL: srv.URL}.Convert(context.TODO(), bytes.NewReader([]byte("raw")))
Expect(err).To(HaveOccurred())
Expect(err).ToNot(MatchError(thumbnailerErrors.ErrNoImageFromRawFile))
})
@@ -173,7 +173,7 @@ func (g Thumbnail) handleCS3Source(ctx context.Context, req *thumbnailssvc.GetTh
"filename": sRes.GetInfo().GetName(),
}
pp := preprocessor.ForType(sRes.GetInfo().GetMimeType(), ppOpts)
img, err := pp.Convert(r)
img, err := pp.Convert(ctx, r)
switch {
case errors.Is(err, terrors.ErrNoImageFromRawFile):
// a raw file without an embedded preview is expected, not an error
@@ -275,7 +275,7 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
"filename": sRes.GetInfo().GetName(),
}
pp := preprocessor.ForType(sRes.GetInfo().GetMimeType(), ppOpts)
img, err := pp.Convert(r)
img, err := pp.Convert(ctx, r)
if img == nil || err != nil {
return "", merrors.NotFound(g.serviceID, "could not get image")
}
@@ -1,6 +1,7 @@
package thumbnail
import (
"context"
"image"
"os"
"path"
@@ -176,7 +177,7 @@ func TestPreviewGenerationTooBigImage(t *testing.T) {
defer f.Close()
preproc := preprocessor.ForType(tt.mimeType, nil)
convert, err := preproc.Convert(f)
convert, err := preproc.Convert(context.TODO(), f)
if err != nil {
return
}