diff --git a/services/thumbnails/pkg/service/http/v0/push.go b/services/thumbnails/pkg/service/http/v0/push.go index 16ad6221c4..579d506f67 100644 --- a/services/thumbnails/pkg/service/http/v0/push.go +++ b/services/thumbnails/pkg/service/http/v0/push.go @@ -43,7 +43,7 @@ var errFormatUnsupported = fmt.Errorf("format not supported by this build") // pushHandler handles POST requests for the imagor-like push-based thumbnail endpoint. // Path patterns (operation and format are both optional; absent operation is the default // center-crop fill, absent format keeps the input's own format): -// - POST /unsafe(/fit-in|/stretch)/{W}x{H}(/filters:{filters}) +// - POST /unsafe(/fill|/fit-in|/stretch)/{W}x{H}(/filters:{filters}) // // The filters segment is captured whole and parsed by outputFormatFromFilters; only the // format filter is meaningful to this executor, other filters are ignored. When no format @@ -176,8 +176,10 @@ func (s Thumbnails) pushHandler(w http.ResponseWriter, r *http.Request) { case "gif": encErr = encodeGIF(&buf, processed) default: - // a format in outputContentTypes that this build has no encoder for - writeNotAcceptable(w, fmt.Sprintf("this build writes no %s", ext)) + // unreachable by construction: ext is a key of outputContentTypes, and + // every key has an arm above. A fifth format would land here before it + // reaches a nil encoder. + writeNotAcceptable(w, fmt.Sprintf("no encoder for %s in this build", ext)) return } diff --git a/services/webdav/README.md b/services/webdav/README.md index 93d5fd7dd9..04aa683c50 100644 --- a/services/webdav/README.md +++ b/services/webdav/README.md @@ -10,7 +10,7 @@ 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. -Generated thumbnails are cached by the webdav service itself. The cache backend defaults to `file`, storing entries under `$OC_BASE_DATA_PATH/thumbnails/files` (override with `WEBDAV_THUMBNAIL_CACHE_BACKEND` and `WEBDAV_THUMBNAIL_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, endpoint and credentials, fails at startup instead of serving without a cache. The `memory` backend is bounded and drops its oldest entries; `none` disables caching on purpose. +Generated thumbnails are cached by the webdav service itself. The cache backend defaults to `file`, storing entries under `$OC_BASE_DATA_PATH/thumbnails/files` (override with `WEBDAV_THUMBNAIL_CACHE_BACKEND` and `WEBDAV_THUMBNAIL_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. A service that is not AWS needs `WEBDAV_THUMBNAIL_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. #### Thumbnail Query String Parameters @@ -48,7 +48,7 @@ The response is therefore not exactly the requested box: rounding up makes it la Normally, an image might get cropped when creating a preview, depending on the aspect ratio of the original image. This can have negative impacts on previews as only a part of the image will be shown. When using an _optional_ processor in the request, cropping can be avoided by defining on how the preview image generation will be done. The following processors are available: - `resize` resizes the image to the specified width and height and returns the transformed image (distorts to the exact box). -- `fit-in` scales the image down to fit within the specified maximum width and height, preserving aspect ratio and never upscaling (letterboxed). +- `fit-in` scales the image down to fit within the specified maximum width and height, preserving aspect ratio and never upscaling. The result is smaller than the box on one axis, it is not padded to it. - `fill`: creates an image with the specified dimensions and fills it with the scaled source image. To achieve the correct aspect ratio without stretching, the source image will be cropped. The following are **legacy aliases** kept for compatibility: @@ -56,7 +56,7 @@ The following are **legacy aliases** kept for compatibility: - `thumbnail` is a legacy alias for `fill` (center-crop to the exact box, may upscale). - `fit` is a legacy alias for `fit-in`. -Naming a processor decides the behavior on its own, legacy alias or imagor name: `a` is overruled and `scalingup` is ignored either way. A request that wants `a` to decide has to leave `processor` out. +Naming one of them decides the behavior on its own, legacy alias or imagor name: `a` is overruled and `scalingup` is ignored either way. A request that wants `a` to decide has to leave `processor` out, a name nobody knows is treated the same way. To apply one of those, a query parameter has to be added to the request, like `?processor=fit-in`. diff --git a/services/webdav/pkg/config/config.go b/services/webdav/pkg/config/config.go index 280eb5551f..fa5c3458df 100644 --- a/services/webdav/pkg/config/config.go +++ b/services/webdav/pkg/config/config.go @@ -41,10 +41,10 @@ type Config struct { ThumbnailCacheS3Endpoint string `yaml:"thumbnail_cache_s3_endpoint" env:"WEBDAV_THUMBNAIL_CACHE_S3_ENDPOINT" desc:"S3 endpoint URL (for MinIO or compatible services) when ThumbnailCacheBackend is 's3'." introductionVersion:"%%NEXT%%"` ThumbnailCacheS3AccessKey string `yaml:"thumbnail_cache_s3_access_key" env:"WEBDAV_THUMBNAIL_CACHE_S3_ACCESS_KEY" desc:"S3 access key for thumbnail cache authentication." introductionVersion:"%%NEXT%%"` ThumbnailCacheS3SecretKey string `yaml:"thumbnail_cache_s3_secret_key" env:"WEBDAV_THUMBNAIL_CACHE_S3_SECRET_KEY" desc:"S3 secret key for thumbnail cache authentication." introductionVersion:"%%NEXT%%"` - ThumbnailSourceTypes []string `yaml:"thumbnail_source_types" env:"OC_THUMBNAIL_SOURCE_TYPES;WEBDAV_THUMBNAIL_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 types the build ships are included; this only adds to them. A type the running generator cannot read answers 404." introductionVersion:"%%NEXT%%"` + ThumbnailSourceTypes []string `yaml:"thumbnail_source_types" env:"OC_THUMBNAIL_SOURCE_TYPES;WEBDAV_THUMBNAIL_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:"%%NEXT%%"` - ThumbnailGeneratorFormats []string `yaml:"thumbnail_generator_formats" env:"OC_THUMBNAIL_GENERATOR_FORMATS;WEBDAV_THUMBNAIL_GENERATOR_FORMATS" desc:"The formats the thumbnail generator writes, as mime:format entries, e.g. image/webp:webp. The format is the name the generator's own format filter uses. A type that is not listed is one the generator cannot write, and a thumbnail mapped to it becomes a JPEG instead." introductionVersion:"%%NEXT%%"` - ThumbnailTypeMapping []string `yaml:"thumbnail_type_mapping" env:"OC_THUMBNAIL_TYPE_MAPPING;WEBDAV_THUMBNAIL_TYPE_MAPPING" desc:"The type a thumbnail is returned in, per type of the image it is made from, as mime:mime entries, e.g. image/vnd.microsoft.icon:image/png. The target is asked for by the name WEBDAV_THUMBNAIL_GENERATOR_FORMATS gives it. A type that is not listed, and one whose target the generator cannot write, becomes a JPEG." introductionVersion:"%%NEXT%%"` + ThumbnailGeneratorFormats []string `yaml:"thumbnail_generator_formats" env:"OC_THUMBNAIL_GENERATOR_FORMATS;WEBDAV_THUMBNAIL_GENERATOR_FORMATS" desc:"The formats the thumbnail generator writes, as mime:format entries, e.g. image/webp:webp. The format is the name the generator's own format filter uses. A type that is not listed is one the generator cannot write, so a mapping onto it is refused at startup." introductionVersion:"%%NEXT%%"` + ThumbnailTypeMapping []string `yaml:"thumbnail_type_mapping" env:"OC_THUMBNAIL_TYPE_MAPPING;WEBDAV_THUMBNAIL_TYPE_MAPPING" desc:"The type a thumbnail is returned in, per type of the image it is made from, as mime:mime entries, e.g. image/vnd.microsoft.icon:image/png. The target is asked for by the name WEBDAV_THUMBNAIL_GENERATOR_FORMATS gives it. A type that is not listed becomes a JPEG; a target the generator cannot write is refused at startup." introductionVersion:"%%NEXT%%"` // the yaml key stays unprefixed, a config file written against it has to keep working ThumbnailFontMapFile string `yaml:"font_map_file" env:"THUMBNAILS_TXT_FONTMAP_FILE;WEBDAV_THUMBNAIL_TXT_FONTMAP_FILE" desc:"The path to a font map file for txt thumbnails." introductionVersion:"%%NEXT%%"` diff --git a/services/webdav/pkg/dav/requests/thumbnail.go b/services/webdav/pkg/dav/requests/thumbnail.go index 086865763d..9da858f89d 100644 --- a/services/webdav/pkg/dav/requests/thumbnail.go +++ b/services/webdav/pkg/dav/requests/thumbnail.go @@ -178,7 +178,8 @@ func aspect(q url.Values) *bool { // targetsExactBox reports whether the processor produces exactly the requested // box, by cropping (fill, thumbnail) or by distorting (resize), which an -// unconstrained axis cannot describe. Only fit-in inscribes instead. +// unconstrained axis cannot describe. fit-in inscribes instead, and so does a +// name nobody knows, which resolves like no processor at all. func targetsExactBox(processor string) bool { switch strings.ToLower(strings.TrimSpace(processor)) { case "fill", "thumbnail", "resize": diff --git a/services/webdav/pkg/service/v0/service.go b/services/webdav/pkg/service/v0/service.go index 34b96b6d13..503140b8a2 100644 --- a/services/webdav/pkg/service/v0/service.go +++ b/services/webdav/pkg/service/v0/service.go @@ -330,7 +330,8 @@ func (g Webdav) requestLogger(r *http.Request) log.Logger { } // parseThumbnailRequest reads the request every thumbnail endpoint starts with. -// It reports false when it already answered with a 400. +// It reports false when it already answered, with a 404 when previews are off +// and a 400 when the request does not parse. func (g Webdav) parseThumbnailRequest(w http.ResponseWriter, r *http.Request, logger log.Logger) (*requests.ThumbnailRequest, bool) { if g.config.DisablePreviews { // every thumbnail path answers 404, which is what the setting promises. @@ -426,16 +427,16 @@ func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// aspectIgnoredHeader is set on thumbnail responses when an explicit processor -// overrode the legacy "a" flag with a different behavior, so developers can tell -// their client to send a consistent request (see the thumbnail docs). +// aspectIgnoredHeader is set on thumbnail responses when what the request +// resolved to contradicts the legacy "a" flag, so developers can tell their +// client to send a consistent request (see the thumbnail docs). const aspectIgnoredHeader = "X-OpenCloud-Thumbnail-Aspect-Ignored" func setAspectIgnoredHeader(w http.ResponseWriter, ignored bool) { if !ignored { return } - w.Header().Set(aspectIgnoredHeader, "1; an explicit processor overrode the legacy 'a' flag, see the thumbnail docs") + w.Header().Set(aspectIgnoredHeader, "1; the processor or the requested box overruled the legacy 'a' flag, see the thumbnail docs") } // handleWorkflowError maps a workflow error onto the response. step names what diff --git a/services/webdav/pkg/thumbnail/cache/cache.go b/services/webdav/pkg/thumbnail/cache/cache.go index b10c50b258..4a5e725450 100644 --- a/services/webdav/pkg/thumbnail/cache/cache.go +++ b/services/webdav/pkg/thumbnail/cache/cache.go @@ -40,7 +40,7 @@ func NewThumbnailCache(backend string, cacheDir string, s3cfg *S3CacheConfig) (T return NewFileCache(dir), nil case "s3": if s3cfg == nil || !s3cfg.IsComplete() { - return nil, errors.New("s3 thumbnail cache needs a bucket, an endpoint and credentials") + return nil, errors.New("s3 thumbnail cache needs a bucket, a region and credentials") } return NewS3Cache(*s3cfg), nil default: diff --git a/services/webdav/pkg/thumbnail/sourcetypes.go b/services/webdav/pkg/thumbnail/sourcetypes.go index 6c4f275cf2..7a085e00b7 100644 --- a/services/webdav/pkg/thumbnail/sourcetypes.go +++ b/services/webdav/pkg/thumbnail/sourcetypes.go @@ -2,6 +2,7 @@ package thumbnail import ( "mime" + "slices" "strings" ) @@ -40,7 +41,7 @@ type SourceTypes struct { // NewSourceTypes merges the configured types onto the defaults. func NewSourceTypes(entries []string) SourceTypes { t := SourceTypes{byType: make(map[string]struct{}, len(DefaultSourceTypes)+len(entries))} - for _, entry := range append(append([]string{}, DefaultSourceTypes...), entries...) { + for _, entry := range slices.Concat(DefaultSourceTypes, entries) { if entry = strings.ToLower(strings.TrimSpace(entry)); entry != "" { t.byType[entry] = struct{}{} } diff --git a/services/webdav/pkg/thumbnail/workflow/workflow.go b/services/webdav/pkg/thumbnail/workflow/workflow.go index 417f6f455c..f4b11d1f47 100644 --- a/services/webdav/pkg/thumbnail/workflow/workflow.go +++ b/services/webdav/pkg/thumbnail/workflow/workflow.go @@ -541,9 +541,9 @@ func (w *ThumbnailWorkflow) sourceImage(ctx context.Context, ref *providerv1beta } // matchOperation resolves the generator resize/crop operation for a request from -// the client's processor and the legacy "a" flag. An explicit processor always -// wins; when no processor is given the default depends on the source type (gifs -// are resized, everything else is filled). webdav has already snapped the box +// the client's processor and the legacy "a" flag. A processor it knows always +// wins; without one, or with a name nobody knows, the default depends on the +// source type (gifs are resized, everything else is filled). webdav has already snapped the box // onto a configured resolution; this only chooses how to fit within that box: // - resize -> stretch (distort to the exact box) // - fill -> fill (center-crop to the box) @@ -552,9 +552,9 @@ func (w *ThumbnailWorkflow) sourceImage(ctx context.Context, ref *providerv1beta // - (none), gif -> stretch (resize for gifs) // - (none), non-gif -> fill (default = thumbnail = fill) // -// "thumbnail" and "fit" are legacy aliases for "fill" and "fit-in". Naming any -// processor decides the behavior on its own, so "a" is overruled and "scalingup" -// is ignored either way. The second return value reports whether upscaling must be +// "thumbnail" and "fit" are legacy aliases for "fill" and "fit-in". Naming one of +// them decides the behavior on its own, so "a" is overruled and "scalingup" is +// ignored either way. The second return value reports whether upscaling must be // suppressed (no_upscale): only fit-in never enlarges its source, so it is true // exactly for the fit/fit-in processors; fill, thumbnail and resize all allow the // generator to enlarge a small source to fill the snapped box. The third return