diff --git a/services/webdav/pkg/preprocessor/preprocessor.go b/services/webdav/pkg/preprocessor/preprocessor.go index 88f70e22a2..7fa944e47c 100644 --- a/services/webdav/pkg/preprocessor/preprocessor.go +++ b/services/webdav/pkg/preprocessor/preprocessor.go @@ -393,9 +393,8 @@ func ForType(mimeType string, opts Options) FileConverter { case "audio/ogg": return AudioDecoder{} default: - // webdav decodes no image: an image/* source is streamed to the - // generator untouched, so a type without a converter here is one - // nobody can make a preview of. + // webdav decodes no image: a type without a converter is streamed to + // the generator untouched, which answers whether it can read it. return nil } } diff --git a/services/webdav/pkg/thumbnail/workflow/workflow.go b/services/webdav/pkg/thumbnail/workflow/workflow.go index f00002e72e..047219fb4b 100644 --- a/services/webdav/pkg/thumbnail/workflow/workflow.go +++ b/services/webdav/pkg/thumbnail/workflow/workflow.go @@ -471,10 +471,9 @@ func (w *ThumbnailWorkflow) generate(ctx context.Context, ref *providerv1beta1.R // imagor's default resize does. genURL := generator.BuildURL(w.generatorURL, int32(box.Dx()), int32(box.Dy()), operation, outputFormat, noUpscale) - // Produce the source image to send to the generator, never decoding an image - // in webdav: real images (incl. gif) are streamed straight through undecoded; - // non-image sources are converted to image bytes here so the generator always - // receives an image. + // Produce the source to send to the generator, never decoding an image in + // webdav: anything without a converter is streamed straight through undecoded, + // the few types with one (text, audio, geogebra) are turned into image bytes here. imgStream, srcType, cleanup, err := w.sourceImage(ctx, ref, auth, mimeType, tr.Filename, logger) if err != nil { logger.Error().Err(err).Msg("could not obtain source image for thumbnail") @@ -505,17 +504,19 @@ func (w *ThumbnailWorkflow) generate(ctx context.Context, ref *providerv1beta1.R return Thumbnail{Data: thumbBytes, ContentType: outputType}, nil } -// sourceImage returns a reader over the source image to send to the generator, -// plus a cleanup func to release any resources (download body, temp buffer). It -// never decodes an image in webdav: -// - real images (image/*, incl. gif) are streamed straight through from storage -// undecoded; the generator handles decoding and multi-frame gifs itself. +// sourceImage returns a reader over the source to send to the generator, plus a +// cleanup func to release any resources (download body, temp buffer). It never +// decodes an image in webdav: +// - a type without a converter (image/*, incl. gif, and whatever else the source +// types announce) is streamed straight through from storage undecoded; the +// generator decides whether it can read it. // - text/plain is rendered to an image here (the only true conversion). // - audio and geogebra sources have their embedded image extracted to bytes. func (w *ThumbnailWorkflow) sourceImage(ctx context.Context, ref *providerv1beta1.Reference, auth, mimeType, filename string, logger log.Logger) (io.Reader, string, func(), error) { m, _, _ := mime.ParseMediaType(mimeType) - if strings.HasPrefix(m, "image/") { + converter := preprocessor.ForType(m, preprocessor.Options{FontMapFile: w.fontMapFile, MaxImageSize: w.maxInputSize}) + if converter == nil { body, err := w.fileDownloader.DownloadStream(ctx, ref, auth) if err != nil { return nil, "", nil, fmt.Errorf("download: %w", err) @@ -534,12 +535,6 @@ func (w *ThumbnailWorkflow) sourceImage(ctx context.Context, ref *providerv1beta return nil, "", nil, fmt.Errorf("read source: %w", err) } - converter := preprocessor.ForType(mimeType, preprocessor.Options{FontMapFile: w.fontMapFile, MaxImageSize: w.maxInputSize}) - if converter == nil { - logger.Debug().Str("mimetype", mimeType).Msg("no converter for this type") - return nil, "", nil, fmt.Errorf("%w: no converter for %s", ErrNotFound, mimeType) - } - img, err := converter.Convert(bytes.NewReader(fileBytes)) if err != nil || img == nil { logger.Debug().Err(err).Msg("could not convert file to image") diff --git a/services/webdav/pkg/thumbnail/workflow/workflow_test.go b/services/webdav/pkg/thumbnail/workflow/workflow_test.go index e264f99228..3621086a9e 100644 --- a/services/webdav/pkg/thumbnail/workflow/workflow_test.go +++ b/services/webdav/pkg/thumbnail/workflow/workflow_test.go @@ -217,6 +217,7 @@ func setupPreprocessTest( if err := r.ParseMultipartForm(10 << 20); err == nil { for _, fh := range r.MultipartForm.File["image"] { captured.filename = filepath.Base(fh.Filename) + captured.contentType = fh.Header.Get("Content-Type") file, ferr := fh.Open() if ferr != nil { continue @@ -235,9 +236,10 @@ func setupPreprocessTest( } type capturedUpload struct { - path string - filename string - data []byte + path string + filename string + contentType string + data []byte } var _ = Describe("ThumbnailWorkflow", func() { @@ -789,6 +791,25 @@ var _ = Describe("ThumbnailWorkflow", func() { Expect(captured.data).To(Equal(pngBytes)) }) + It("streams an announced type without a converter through untouched", func() { + pdfBytes := []byte("%PDF-1.4 not really a pdf") + + wf2, captured := setupPreprocessTest(GinkgoT(), gatewayClient, logger, testCache, "application/pdf", "doc.pdf", pdfBytes) + wf2.sourceTypes = thumbnail.NewSourceTypes([]string{"application/pdf"}) + + tr := &requests.ThumbnailRequest{ + Ref: spaceRef("storageid$spaceid!opaqueid", "doc.pdf"), + Filename: "doc.pdf", + Width: 64, + Height: 64, + } + + _, err := wf2.Execute(context.Background(), tr, testToken, logger) + Expect(err).ToNot(HaveOccurred()) + Expect(captured.data).To(Equal(pdfBytes)) + Expect(captured.contentType).To(Equal("application/pdf"), "the generator sees the file's own type, not a guess") + }) + It("converts audio to cover art before posting", func() { mp3Bytes, err := os.ReadFile(filepath.Join("..", "..", "preprocessor", "test_assets", "empty.mp3")) Expect(err).ToNot(HaveOccurred()) @@ -997,6 +1018,13 @@ var _ = Describe("ThumbnailWorkflow", func() { Height: 64, } + untouched := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + defer GinkgoRecover() + Fail("a type outside the source types must never reach the generator") + })) + defer untouched.Close() + wf.generatorURL = untouched.URL + _, err := wf.Execute(context.Background(), tr, testToken, logger) Expect(err).To(HaveOccurred()) Expect(errors.Is(err, ErrUnsupportedFileType)).To(BeTrue())