diff --git a/services/webdav/README.md b/services/webdav/README.md index 0fca927c79..13eb7fa8b1 100644 --- a/services/webdav/README.md +++ b/services/webdav/README.md @@ -10,7 +10,15 @@ Currently, the webdav service handles request for two functionalities, which are The webdav service provides various `GET` endpoints to get the thumbnails of a file in authenticated and unauthenticated contexts. It also provides thumbnails for spaces on different endpoints. -When `OC_TIKA_URL` (or `WEBDAV_TIKA_URL`) is set, an Apache Tika server (4.2.0 or newer) supplies the preview image for documents, raw photos, audio cover art and other types. Which types go there is a list, and the default is the formats a Tika release serves a thumbnail for: documents, ebooks, office and iWork files, audio cover art and the camera raws. `OC_TIKA_THUMBNAIL_MIME_TYPES` (or `WEBDAV_TIKA_THUMBNAIL_MIME_TYPES`) replaces that list, `OC_TIKA_ADDITIONAL_THUMBNAIL_MIME_TYPES` adds to it, a single `*` in either sends every type that nothing else claims, and an empty list sends nothing at all, which leaves a Tika server shared with the search service to the search service alone. An entry like `image/x-nikon-nef:image/x-raw-nikon` maps a type to the one Tika knows the format by. Tika takes a declared type only when it refines what it detected itself, so an entry pins the leaf type its detector left at the parent, a raw photo it only read as a plain tiff. The type decides which parser reads the file: a raw read as a tiff is parsed as a tiff, its maker notes stay unread, and the embedded preview a thumbnail would be made of is never found. The formats in the default list are detected down to the leaf type, so none of them needs the hint, and it is there for the next format whose detection stops one level short. The webdav service calls `PUT /unpack/preset/thumbnail`, so the Tika server has to run with the `thumbnail` catalog preset active: start it with `TIKA_PRESETS=thumbnail`, a comma separated list of the presets to activate. Without Tika, audio cover art and GeoGebra thumbnails are still extracted by the webdav service itself; those built-in extractors are deprecated. A type Tika takes is attempted even when `OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES` does not announce it, so a direct request can get a preview that a listing reports as absent. Who gets a file is decided in one order, first match wins: a type the Tika list names goes to Tika, which is why the camera raws are named there, reva types them as images and no generator reads them; then an image the generator reads is handed over as it is, and for an image type that is what `OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES` says, an imagor with ImageMagick reads heic where the built-in one does not; then the built-in extractors take text, audio and GeoGebra; then a `*` takes whatever is left. A type nothing claims answers `404` without reading the file at all. Naming a type also restricts Tika to the named ones, so a deployment that sends raw photos to Tika has to name the document types it wants from Tika as well, and an empty list sends nothing at all, which leaves the Tika server to the search service alone. The thumbnail is written in the type the **file's** own type maps to (`OC_THUMBNAILS_ADDITIONAL_OUTPUT_TYPES`), not in whatever type Tika hands back, so the answer is the same before and after a cache hit. That is also why a windows binary is mapped to `image/png` by default: the icon Tika reads out of it carries transparency that a JPEG would flatten onto black. +When `OC_TIKA_URL` (or `WEBDAV_TIKA_URL`) is set, an Apache Tika server (4.2.0 or newer) supplies the preview image for documents, raw photos, audio cover art and other types. Which types go there is a list, and the default is the formats a Tika release serves a thumbnail for: documents, ebooks, office and iWork files, audio cover art and the camera raws. `OC_TIKA_THUMBNAIL_MIME_TYPES` (or `WEBDAV_TIKA_THUMBNAIL_MIME_TYPES`) replaces that list, `OC_TIKA_ADDITIONAL_THUMBNAIL_MIME_TYPES` adds to it, a single `*` in either sends every type that nothing else claims, and an empty list sends nothing at all, which leaves a Tika server shared with the search service to the search service alone. An entry like `image/x-nikon-nef:image/x-raw-nikon` maps a type to the one Tika knows the format by. Tika takes a declared type only when it refines what it detected itself, so an entry pins the leaf type its detector left at the parent, a raw photo it only read as a plain tiff. The type decides which parser reads the file: a raw read as a tiff is parsed as a tiff, its maker notes stay unread, and the embedded preview a thumbnail would be made of is never found. The formats in the default list are detected down to the leaf type, so none of them needs the hint, and it is there for the next format whose detection stops one level short. The webdav service calls `PUT /unpack/preset/thumbnail`, so the Tika server has to run with the `thumbnail` catalog preset active: start it with `TIKA_PRESETS=thumbnail`, a comma separated list of the presets to activate. Without Tika, audio cover art and GeoGebra thumbnails are still extracted by the webdav service itself; those built-in extractors are deprecated. A type Tika takes is attempted even when `OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES` does not announce it, so a direct request can get a preview that a listing reports as absent. Who gets a file is decided in one order, first match wins, and every type has exactly one taker: + +1. a type the Tika list names goes to Tika, an image too, which is why the camera raws are named there: reva types them as images and no generator reads them +2. text, audio and GeoGebra go to the built-in extractors +3. an image goes to the generator as it is; whether it can read it is the generator's answer, an imagor with ImageMagick reads heic where the built-in one does not +4. anything else goes to Tika when the Tika list has `*` +5. and otherwise to the generator as it is + +So `*` never sends an image: images belong to the generator, and sending one to Tika would only move the file. An image type that only Tika can read has to be named in the Tika list. Announcing a type in `OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES` and sending it to Tika are separate decisions: the announcement is what `oc:has-preview` reports, the Tika list says who makes the image. Naming a type also restricts Tika to the named ones, so a deployment that sends raw photos to Tika has to name the document types it wants from Tika as well, and an empty list sends nothing at all, which leaves the Tika server to the search service alone. The thumbnail is written in the type the **file's** own type maps to (`OC_THUMBNAILS_ADDITIONAL_OUTPUT_TYPES`), not in whatever type Tika hands back, so the answer is the same before and after a cache hit. That is also why a windows binary is mapped to `image/png` by default: the icon Tika reads out of it carries transparency that a JPEG would flatten onto black. Generated thumbnails are cached by the webdav service itself. The cache backend defaults to `file`, storing entries under `$OC_BASE_DATA_PATH/thumbnails/cache` (override with `WEBDAV_THUMBNAILS_CACHE_BACKEND` and `WEBDAV_THUMBNAILS_CACHE_DIR`). Use the `s3` backend when running multiple instances behind a load balancer so they share one cache. A backend nobody can serve, an unknown name or an `s3` backend without bucket, region and credentials, fails at startup instead of serving without a cache, as long as previews are enabled. A service that is not AWS needs `WEBDAV_THUMBNAILS_CACHE_S3_ENDPOINT` too, and it is always addressed over https. The `memory` backend is bounded and drops its oldest entries; `none` disables caching on purpose. The cache also keeps the image an extractor made of a file, under one entry per file, so a second size does not extract again and a file that carries no image is only asked once. Nothing expires: a changed file is a new entry because the key carries its checksum, but an extractor that learns to read a format it could not read before does not reach the files it already answered for. After such an upgrade, an Apache Tika release among them, empty the cache (`WEBDAV_THUMBNAILS_CACHE_DIR`, or the bucket) to have them asked again. diff --git a/services/webdav/pkg/config/config.go b/services/webdav/pkg/config/config.go index e63d31db42..e74872134f 100644 --- a/services/webdav/pkg/config/config.go +++ b/services/webdav/pkg/config/config.go @@ -29,8 +29,8 @@ type Config struct { Thumbnails Thumbnails `yaml:"thumbnails"` TikaURL string `yaml:"tika_url" env:"OC_TIKA_URL;WEBDAV_TIKA_URL" desc:"URL of an Apache Tika server. When set, thumbnails are generated for documents, raw photos, audio cover art and other formats Tika can extract a preview from. Needs Tika 4.2.0 or newer, started with TIKA_PRESETS=thumbnail. A 4.1.x server works as well, with the preset activated in its config file instead." introductionVersion:"%%NEXT%%"` - TikaThumbnailMimeTypes []string `yaml:"tika_thumbnail_mime_types" env:"OC_TIKA_THUMBNAIL_MIME_TYPES;WEBDAV_TIKA_THUMBNAIL_MIME_TYPES" desc:"The mime types sent to Tika for their thumbnail, which replaces the built-in list of the formats a Tika release serves one for. The camera raws are in that list because reva types them as images and an image is handed to the generator as it is unless something claims it. A single '*' sends every type that nothing else claims, and an empty list sends nothing at all, which leaves a Tika shared with the search service to the search service alone. An entry can map a type to the one Tika knows the format by, e.g. image/x-nikon-nef:image/x-raw-nikon. Tika takes a declared type only when it refines what it detected itself, so an entry pins the leaf type its detector left at the parent, a raw photo it only read as a plain tiff. The type decides which parser reads the file, so a raw read as a tiff loses its maker notes and with them the embedded preview; the formats shipped here are detected down to the leaf type, so no entry is needed for them." introductionVersion:"%%NEXT%%"` - AdditionalTikaThumbnailMimeTypes []string `yaml:"additional_tika_thumbnail_mime_types" env:"OC_TIKA_ADDITIONAL_THUMBNAIL_MIME_TYPES;WEBDAV_TIKA_ADDITIONAL_THUMBNAIL_MIME_TYPES" desc:"Additional mime types sent to Tika for their thumbnail, on top of the built-in list or of whatever OC_TIKA_THUMBNAIL_MIME_TYPES names instead. An entry carries a type hint the same way." introductionVersion:"%%NEXT%%"` + TikaThumbnailMimeTypes []string `yaml:"tika_thumbnail_mime_types" env:"OC_TIKA_THUMBNAIL_MIME_TYPES;WEBDAV_TIKA_THUMBNAIL_MIME_TYPES" desc:"The mime types sent to Tika for their thumbnail, which replaces the built-in list of the formats a Tika release serves one for. A named type goes to Tika before anything else, an image too: the camera raws are in that list because reva types them as images and an image the list does not name goes to the generator. A single '*' sends every other type that is neither an image nor has a built-in converter, so it never sends an image, and an empty list sends nothing at all, which leaves a Tika shared with the search service to the search service alone. An entry can map a type to the one Tika knows the format by, e.g. image/x-nikon-nef:image/x-raw-nikon. Tika takes a declared type only when it refines what it detected itself, so an entry pins the leaf type its detector left at the parent, a raw photo it only read as a plain tiff. The type decides which parser reads the file, so a raw read as a tiff loses its maker notes and with them the embedded preview; the formats shipped here are detected down to the leaf type, so no entry is needed for them." introductionVersion:"%%NEXT%%"` + AdditionalTikaThumbnailMimeTypes []string `yaml:"additional_tika_thumbnail_mime_types" env:"OC_TIKA_ADDITIONAL_THUMBNAIL_MIME_TYPES;WEBDAV_TIKA_ADDITIONAL_THUMBNAIL_MIME_TYPES" desc:"Additional mime types sent to Tika for their thumbnail, on top of the built-in list or of whatever OC_TIKA_THUMBNAIL_MIME_TYPES names instead. An entry carries a type hint the same way. An image type that only Tika can read has to be named here or there, '*' never sends an image." introductionVersion:"%%NEXT%%"` Context context.Context `yaml:"-"` } @@ -54,7 +54,7 @@ type Thumbnails struct { CacheS3Endpoint string `yaml:"cache_s3_endpoint" env:"WEBDAV_THUMBNAILS_CACHE_S3_ENDPOINT" desc:"S3 endpoint URL (for MinIO or compatible services) when WEBDAV_THUMBNAILS_CACHE_BACKEND is 's3'." introductionVersion:"9.0.0"` CacheS3AccessKey string `yaml:"cache_s3_access_key" env:"WEBDAV_THUMBNAILS_CACHE_S3_ACCESS_KEY" desc:"S3 access key for thumbnail cache authentication." introductionVersion:"9.0.0"` CacheS3SecretKey string `yaml:"cache_s3_secret_key" env:"WEBDAV_THUMBNAILS_CACHE_S3_SECRET_KEY" desc:"S3 secret key for thumbnail cache authentication." introductionVersion:"9.0.0"` - AdditionalSourceTypes []string `yaml:"additional_source_types" env:"OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES;WEBDAV_THUMBNAILS_ADDITIONAL_SOURCE_TYPES" desc:"Additional mime types a preview is announced and attempted for, e.g. image/vnd.microsoft.icon for a generator that reads icons. The built-in list is included; this only adds to them. A type the running generator cannot read answers 404." introductionVersion:"9.0.0"` + AdditionalSourceTypes []string `yaml:"additional_source_types" env:"OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES;WEBDAV_THUMBNAILS_ADDITIONAL_SOURCE_TYPES" desc:"Additional mime types a preview is announced and attempted for, e.g. image/vnd.microsoft.icon for a generator that reads icons. The built-in list is included; this only adds to them. An announced type that the Tika list does not name and no built-in converter takes goes to the generator as it is, an image always and anything else unless the Tika list has '*'; a type the running generator cannot read answers 404. Announcing a type does not send it to Tika, naming it in the Tika list does." introductionVersion:"9.0.0"` AdditionalGeneratorFormats []string `yaml:"additional_generator_formats" env:"OC_THUMBNAILS_ADDITIONAL_GENERATOR_FORMATS;WEBDAV_THUMBNAILS_ADDITIONAL_GENERATOR_FORMATS" desc:"Additional formats the thumbnail generator writes, one per type it writes, as type:format entries, e.g. image/webp:webp. The format is the name the generator's own format filter uses. The built-in list is included; an entry for a type that is already listed replaces it. A type that is not listed is asked for as a JPEG instead, because what the generator behind the URL really writes is its own business." introductionVersion:"9.0.0"` AdditionalOutputTypes []string `yaml:"additional_output_types" env:"OC_THUMBNAILS_ADDITIONAL_OUTPUT_TYPES;WEBDAV_THUMBNAILS_ADDITIONAL_OUTPUT_TYPES" desc:"Additional types a thumbnail is returned in, one per type of the image it is made from, as source:target entries, e.g. image/vnd.microsoft.icon:image/png. The built-in list is included; an entry for a source type that is already listed replaces it. The target is asked for by the name WEBDAV_THUMBNAILS_ADDITIONAL_GENERATOR_FORMATS gives it. A type that is not listed becomes a JPEG; a target this service has no format name for is asked for as a JPEG as well." introductionVersion:"9.0.0"` diff --git a/services/webdav/pkg/preprocessor/preprocessor.go b/services/webdav/pkg/preprocessor/preprocessor.go index 1003a7f832..c5886ac35d 100644 --- a/services/webdav/pkg/preprocessor/preprocessor.go +++ b/services/webdav/pkg/preprocessor/preprocessor.go @@ -362,43 +362,36 @@ type Options struct { FontFace *opentype.FaceOptions MaxImageSize uint64 Tika thumbnail.Tika - SourceTypes thumbnail.SourceTypes Filename string } -// tikaOr prefers a configured Tika server, else the fallback -func tikaOr(opts Options, mimeType string, fallback FileConverter) FileConverter { - if !opts.Tika.Supports(mimeType) { - return fallback - } - +func toTika(opts Options, mimeType string) FileConverter { return TikaThumbnail{tikaURL: opts.Tika.URL, filename: opts.Filename, contentType: opts.Tika.ContentType(mimeType)} } // ForType says what has to happen to a file of this type before the generator -// sees it: a converter to run, nil when the generator reads the file as it is, -// and ErrNoThumbnail when nothing here can make an image of it. +// sees it: a converter to run, or nil when the file goes to the generator as it +// is, which answers whether it can read it. Only types that passed the gate +// get here: announced ones, and those the Tika list takes. // -// The order is the answer to two questions that look the same and are not, what -// a type is and who can serve it: -// 1. a type the configuration names for Tika goes to Tika, an image too, which -// is how a raw photo is extracted instead of being handed to a generator -// that cannot read it -// 2. an image the generator reads is handed over untouched -// 3. a type with a converter here is converted here +// Every type has exactly one taker, the first that applies: +// 1. a type the Tika list names goes to Tika, an image too, which is how a raw +// photo is extracted instead of being handed to a generator that cannot +// read it +// 2. a type with a converter here is converted here +// 3. an image goes to the generator // 4. what is left goes to Tika when it was told to take everything -// 5. and otherwise nothing can make a preview of it -func ForType(mimeType string, opts Options) (FileConverter, error) { +// 5. and otherwise to the generator +// +// So "*" never sends an image: images belong to the generator, and sending one +// to Tika would move the file for nothing. +func ForType(mimeType string, opts Options) FileConverter { // We can ignore the error here because the caller parses it as well, so we // only get here when the mimeType parses fine. mimeType, _, _ = mime.ParseMediaType(mimeType) if opts.Tika.ConfiguredFor(mimeType) { - return tikaOr(opts, mimeType, nil), nil - } - - if opts.SourceTypes.GeneratorReads(mimeType) { - return nil, nil + return toTika(opts, mimeType) } switch mimeType { @@ -419,18 +412,22 @@ func ForType(mimeType string, opts Options) (FileConverter, error) { fontLoader, _ = NewFontLoader("", fontFace) } - return TxtToImageConverter{fontLoader: fontLoader}, nil + return TxtToImageConverter{fontLoader: fontLoader} case "application/vnd.geogebra.slides": - return tikaOr(opts, mimeType, GgsDecoder{thumbnailpath: "_slide0/geogebra_thumbnail.png", maxSize: maxExtractedSize(opts)}), nil + return GgsDecoder{thumbnailpath: "_slide0/geogebra_thumbnail.png", maxSize: maxExtractedSize(opts)} case "application/vnd.geogebra.pinboard": - return tikaOr(opts, mimeType, GgpDecoder{}), nil + return GgpDecoder{} case "audio/flac", "audio/mpeg", "audio/ogg": - return tikaOr(opts, mimeType, AudioDecoder{}), nil + return AudioDecoder{} } - if converter := tikaOr(opts, mimeType, nil); converter != nil { - return converter, nil + if strings.HasPrefix(mimeType, "image/") { + return nil } - return nil, ErrNoThumbnail + if opts.Tika.TakesAll() { + return toTika(opts, mimeType) + } + + return nil } diff --git a/services/webdav/pkg/preprocessor/preprocessor_test.go b/services/webdav/pkg/preprocessor/preprocessor_test.go index 436f738c58..08cffabb77 100644 --- a/services/webdav/pkg/preprocessor/preprocessor_test.go +++ b/services/webdav/pkg/preprocessor/preprocessor_test.go @@ -165,7 +165,7 @@ var _ = Describe("preprocessor", func() { }) Describe("ForType", func() { - It("has no converter for an image, which never reaches the preprocessor", func() { + It("hands an image to the generator as it is", func() { Expect(ForType("image/png", Options{})).To(BeNil()) }) @@ -181,16 +181,8 @@ var _ = Describe("preprocessor", func() { Expect(ForType("text/plain", Options{})).To(BeAssignableToTypeOf(TxtToImageConverter{})) }) - It("refuses a type nothing here can read", func() { - converter, err := ForType("application/pdf", Options{}) - Expect(err).To(MatchError(ErrNoThumbnail), "without a Tika server nothing makes an image of a pdf") - Expect(converter).To(BeNil()) - }) - - It("refuses an image the generator does not read, instead of handing it over", func() { - converter, err := ForType("image/x-nikon-nef", Options{}) - Expect(err).To(MatchError(ErrNoThumbnail)) - Expect(converter).To(BeNil()) + It("hands an announced type without a converter to the generator, which says whether it reads it", func() { + Expect(ForType("application/pdf", Options{})).To(BeNil()) }) }) }) diff --git a/services/webdav/pkg/preprocessor/tika_test.go b/services/webdav/pkg/preprocessor/tika_test.go index 7f1ff9304d..fee3aaa6e4 100644 --- a/services/webdav/pkg/preprocessor/tika_test.go +++ b/services/webdav/pkg/preprocessor/tika_test.go @@ -11,6 +11,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" "github.com/opencloud-eu/opencloud/services/webdav/pkg/thumbnail" ) @@ -138,45 +139,43 @@ var _ = Describe("TikaThumbnail", func() { }) var _ = Describe("ForType with a Tika server", func() { - sourceTypes := thumbnail.NewSourceTypes(nil) + const tikaURL = "http://tika:9998" + named := thumbnail.NewTika(tikaURL, thumbnail.DefaultTikaTypes, nil) + everything := thumbnail.NewTika(tikaURL, []string{"*"}, nil) + off := thumbnail.NewTika("", thumbnail.DefaultTikaTypes, nil) - It("sends what the list names to Tika, even an image, and leaves the generator its own", func() { - opts := Options{Tika: thumbnail.NewTika("http://tika:9998", thumbnail.DefaultTikaTypes, nil), SourceTypes: sourceTypes, Filename: "song.mp3"} - Expect(ForType("audio/mpeg", opts)).To(BeAssignableToTypeOf(TikaThumbnail{})) - Expect(ForType("application/vnd.geogebra.slides", opts)).To(BeAssignableToTypeOf(TikaThumbnail{})) - Expect(ForType("application/pdf", opts)).To(BeAssignableToTypeOf(TikaThumbnail{})) - Expect(ForType("image/x-nikon-nef", opts)).To(BeAssignableToTypeOf(TikaThumbnail{}), "a raw is named, so it is extracted instead of handed over") - Expect(ForType("text/plain", opts)).To(BeAssignableToTypeOf(TxtToImageConverter{})) - Expect(ForType("image/png", opts)).To(BeNil(), "the generator reads a png itself") - }) + tika := BeAssignableToTypeOf(TikaThumbnail{}) + generator := BeNil() - It("hands an image type the deployment announced to the generator", func() { - opts := Options{ - Tika: thumbnail.NewTika("http://tika:9998", thumbnail.DefaultTikaTypes, nil), - SourceTypes: thumbnail.NewSourceTypes([]string{"image/heic"}), - } - Expect(ForType("image/heic", opts)).To(BeNil(), "a generator that reads heic is told so through the source types") - }) + for _, c := range []struct { + name string + tika thumbnail.Tika + mimeType string + taker types.GomegaMatcher + }{ + {"a named document goes to Tika", named, "application/pdf", tika}, + {"a named image goes to Tika too", named, "image/x-nikon-nef", tika}, + {"a named type with a converter goes to Tika", named, "audio/mpeg", tika}, + {"a converter takes what the list does not name", named, "text/plain", BeAssignableToTypeOf(TxtToImageConverter{})}, + {"an image goes to the generator", named, "image/png", generator}, + {"an unnamed image goes to the generator", named, "image/heic", generator}, + {"an unnamed document goes to the generator", named, "application/x-anything", generator}, - It("takes what nothing else claims when it is told to take everything", func() { - opts := Options{Tika: thumbnail.NewTika("http://tika:9998", []string{"*"}, nil), SourceTypes: sourceTypes} - Expect(ForType("application/x-anything", opts)).To(BeAssignableToTypeOf(TikaThumbnail{})) - Expect(ForType("image/x-nikon-nef", opts)).To(BeAssignableToTypeOf(TikaThumbnail{}), "an image the generator does not read is not an exception") - Expect(ForType("image/jpeg", opts)).To(BeNil(), "but one it reads never travels to Tika") - }) + {"the catch-all takes a document", everything, "application/x-anything", tika}, + {"the catch-all never takes an image", everything, "image/x-nikon-nef", generator}, + {"nor a jpeg", everything, "image/jpeg", generator}, + {"nor what a converter takes", everything, "audio/mpeg", BeAssignableToTypeOf(AudioDecoder{})}, - It("keeps the in-process converters without a Tika server", func() { - Expect(ForType("audio/mpeg", Options{})).To(BeAssignableToTypeOf(AudioDecoder{})) - Expect(ForType("application/vnd.geogebra.slides", Options{})).To(BeAssignableToTypeOf(GgsDecoder{})) - }) + {"without a Tika URL nothing goes to Tika", off, "application/pdf", generator}, + {"and the converters are back", off, "application/vnd.geogebra.slides", BeAssignableToTypeOf(GgsDecoder{})}, + } { + It(c.name, func() { + Expect(ForType(c.mimeType, Options{Tika: c.tika})).To(c.taker) + }) + } It("hands the mapped type to Tika", func() { - opts := Options{Tika: thumbnail.NewTika("http://tika:9998", []string{"application/pdf", "image/x-nikon-nef:image/x-raw-nikon"}, nil), SourceTypes: sourceTypes} - Expect(ForType("application/pdf", opts)).To(BeAssignableToTypeOf(TikaThumbnail{})) - Expect(ForType("audio/mpeg", opts)).To(BeAssignableToTypeOf(AudioDecoder{}), "not in the configured list") - - converter, err := ForType("image/x-nikon-nef", opts) - Expect(err).ToNot(HaveOccurred()) - Expect(converter.(TikaThumbnail).contentType).To(Equal("image/x-raw-nikon")) + t := thumbnail.NewTika(tikaURL, []string{"application/pdf", "image/x-nikon-nef:image/x-raw-nikon"}, nil) + Expect(ForType("image/x-nikon-nef", Options{Tika: t}).(TikaThumbnail).contentType).To(Equal("image/x-raw-nikon")) }) }) diff --git a/services/webdav/pkg/thumbnail/sourcetypes.go b/services/webdav/pkg/thumbnail/sourcetypes.go index cdc11cf3c9..566162064b 100644 --- a/services/webdav/pkg/thumbnail/sourcetypes.go +++ b/services/webdav/pkg/thumbnail/sourcetypes.go @@ -6,9 +6,8 @@ import ( "strings" ) -// GeneratorImageTypes are the image types both resize backends read, so webdav -// hands the file to the generator as it is. Every other type needs something to -// make an image of it first, a preprocessor here or a Tika server. +// GeneratorImageTypes are the image types both resize backends read, announced +// without further configuration. var GeneratorImageTypes = []string{ "image/png", "image/jpg", @@ -54,22 +53,6 @@ func NewSourceTypes(entries []string) SourceTypes { return t } -// GeneratorReads reports whether the generator is handed a file of this type as -// it is. Only images are, and which ones is what the source types say: the -// built-in list is what both resize backends read, a deployment adds the types -// its own generator reads on top of it. -func (t SourceTypes) GeneratorReads(contentType string) bool { - parsed, _, err := mime.ParseMediaType(contentType) - if err != nil || !strings.HasPrefix(parsed, "image/") { - return false - } - if len(t.byType) == 0 { - return slices.Contains(GeneratorImageTypes, parsed) - } - - return t.Supports(parsed) -} - // Supports reports whether a preview is made of this type. func (t SourceTypes) Supports(contentType string) bool { parsed, _, err := mime.ParseMediaType(contentType) diff --git a/services/webdav/pkg/thumbnail/sourcetypes_test.go b/services/webdav/pkg/thumbnail/sourcetypes_test.go index 26dee184dc..042abb7d3a 100644 --- a/services/webdav/pkg/thumbnail/sourcetypes_test.go +++ b/services/webdav/pkg/thumbnail/sourcetypes_test.go @@ -11,22 +11,16 @@ var _ = Describe("SourceTypes", func() { defaults := thumbnail.NewSourceTypes(nil) withHeic := thumbnail.NewSourceTypes([]string{"image/heic"}) - DescribeTable("GeneratorReads", + DescribeTable("Supports", func(types thumbnail.SourceTypes, mimeType string, want bool) { - Expect(types.GeneratorReads(mimeType)).To(Equal(want)) + Expect(types.Supports(mimeType)).To(Equal(want)) }, Entry("an image both backends read", defaults, "image/jpeg", true), Entry("with parameters", defaults, "image/png; charset=binary", true), - Entry("not a camera raw, which is an image nobody renders", defaults, "image/x-nikon-nef", false), - Entry("not a document, whatever the list says", defaults, "application/pdf", false), - Entry("not audio, which is announced but extracted", defaults, "audio/mpeg", false), + Entry("built-in audio", defaults, "audio/mpeg", true), + Entry("not a camera raw by default", defaults, "image/x-nikon-nef", false), Entry("an image type the deployment added", withHeic, "image/heic", true), Entry("and the built-in ones are still in there", withHeic, "image/tiff", true), Entry("an unparsable type", defaults, "not a mime", false), ) - - It("falls back to the built-in image types when nothing was configured", func() { - Expect(thumbnail.SourceTypes{}.GeneratorReads("image/jpeg")).To(BeTrue()) - Expect(thumbnail.SourceTypes{}.GeneratorReads("image/heic")).To(BeFalse()) - }) }) diff --git a/services/webdav/pkg/thumbnail/tika.go b/services/webdav/pkg/thumbnail/tika.go index 93be89437e..d0e7ca22ff 100644 --- a/services/webdav/pkg/thumbnail/tika.go +++ b/services/webdav/pkg/thumbnail/tika.go @@ -101,10 +101,9 @@ func (t Tika) Supports(mimeType string) bool { return all } -// ConfiguredFor reports whether OC_TIKA_THUMBNAIL_MIME_TYPES lists this type, -// as opposed to the catch-all taking it. An image is only taken away from the -// generator when the configuration asks for it, because the generator may well -// read the type itself. +// ConfiguredFor reports whether the Tika list names this type, as opposed to +// the catch-all taking it. Only a named type is taken away from a converter or +// the generator. func (t Tika) ConfiguredFor(mimeType string) bool { if t.URL == "" { return false @@ -118,6 +117,17 @@ func (t Tika) ConfiguredFor(mimeType string) bool { return named } +// TakesAll reports whether the Tika list has the catch-all "*", which takes +// every announced type that is neither an image nor has a converter. +func (t Tika) TakesAll() bool { + if t.URL == "" { + return false + } + _, all := t.mimeTypes["*"] + + return all +} + // ContentType is the type Tika is told, mapped where configured. Tika takes a // declared type only when it refines what it detected itself, so a mapping // cannot overrule it; what it can do is pin the leaf type a detector left at diff --git a/services/webdav/pkg/thumbnail/workflow/workflow.go b/services/webdav/pkg/thumbnail/workflow/workflow.go index af039313ce..c472de429c 100644 --- a/services/webdav/pkg/thumbnail/workflow/workflow.go +++ b/services/webdav/pkg/thumbnail/workflow/workflow.go @@ -488,10 +488,9 @@ func (w *ThumbnailWorkflow) generate(ctx context.Context, ref *providerv1beta1.R } } - // 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: what Tika or a converter here takes is turned into image bytes, the + // rest is streamed straight through undecoded. imgStream, srcType, cleanup, err := w.sourceImage(ctx, ref, auth, mimeType, tr.Filename, checksum, logger) if err != nil { if errors.Is(err, ErrNoThumbnail) { @@ -543,30 +542,24 @@ func (w *ThumbnailWorkflow) generate(ctx context.Context, ref *providerv1beta1.R // 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. +// - a type preprocessor.ForType gives no converter (images, and whatever else +// is announced and not taken by Tika) is streamed straight through from +// storage undecoded; the generator decides whether it can read it. +// - a type Tika takes has its image extracted there. +// - text/plain is rendered to an image here, audio and geogebra sources have +// their embedded image extracted to bytes. func (w *ThumbnailWorkflow) sourceImage(ctx context.Context, ref *providerv1beta1.Reference, auth, mimeType, filename, checksum string, logger log.Logger) (io.Reader, string, func(), error) { m, _, _ := mime.ParseMediaType(mimeType) // the preprocessor decides what a type needs. No converter means the file // is handed to the generator as it is, which is the case for every type it // reads itself. - converter, err := preprocessor.ForType(mimeType, preprocessor.Options{ + converter := preprocessor.ForType(mimeType, preprocessor.Options{ FontMapFile: w.fontMapFile, MaxImageSize: w.maxInputSize, Tika: w.tika, - SourceTypes: w.sourceTypes, Filename: filename, }) - if err != nil { - // nothing can make an image of this type, so there is nothing to fetch - logger.Debug().Str("mimetype", mimeType).Msg("no preview for this type") - - return nil, "", nil, ErrNoThumbnail - } // what the extraction found is remembered per file, so a second size does // not send the file to Tika again, and a file that carries nothing is only