Skip to content

Push based thumbnailer architecture - #3397

Draft
butonic wants to merge 18 commits into
mainfrom
imagor
Draft

Push based thumbnailer architecture#3397
butonic wants to merge 18 commits into
mainfrom
imagor

Conversation

@butonic

@butonic butonic commented Aug 24, 2026

Copy link
Copy Markdown
Member

This PR tries to fix #1128 in a backwards compatible way.

Thumbnail generation used to live entirely in the thumbnails service behind a gRPC API: webdav called GetThumbnail, the service fetched the source from storage, preprocessed and generated the image, stored it on its own filesystem, and returned a JWT-signed URL that webdav had to follow with a second authenticated HTTP call just to get the bytes back. This branch inverts that: the thumbnails service becomes a stateless imagor-compatible resizer (one POST endpoint — image bytes in, resized bytes out; no auth, storage, or gRPC), and webdav owns the whole workflow via a single ThumbnailWorkflow type: stat via gateway → cache check → download source → preprocess → POST to generator → cache → respond.

Webdav gains what it needs to own that pipeline: the preprocessors (PDF→image, text→image, audio cover art) moved over from the thumbnails service, a new checksum-keyed thumbnail cache with memory/file/S3/noop backends, and a pkg/generator package that builds resizer URLs and posts multipart images. Config shrinks to one generator URL plus timeout, optional auth header (for an external imagor behind a proxy), and max input file size — the URL can point at the built-in thumbnails service or any imagor instance.

The old architecture is deleted from the thumbnails service: proto files, gRPC handler, JWT transfer tokens, filesystem storage, source fetchers, the /data endpoint, and duplicated utilities (~4,000 lines), plus leftover dead config, no-op metrics wrappers, and opencloud init's now-meaningless transfer-secret generation. Net diff: 102 files, +3,337/−4,336; thumbnail requests no longer need the second round-trip, and the resizer is trivially replaceable.

Related:
#630
#3364
#3332
opencloud-eu/reva#781
opencloud-eu/reva#773

Discussion:
https://github.com/orgs/opencloud-eu/discussions/2368
https://github.com/orgs/opencloud-eu/discussions/1090

@dschmidt This PR is not ready, but I want to bring your attention to this approach, which is why I am pushing this code now.

butonic added 15 commits August 24, 2026 16:42
- Add introductionVersion tags to all new config fields
- Fix %w error wrapping in statFile (was using %s/bare string)
- Remove dead resolveStatPath function
- Invert ThumbnailBackend switch so default creates gRPC client
  (unknown values now fall through to gRPC, matching spec intent)
- Extract shouldUseImagor() method to eliminate duplicated dispatch guard
- Fix parseMaxInputFileSize: use TrimSuffix instead of TrimRight
  (character-set trimming broke MB/KB suffixes)
- Add ThumbnailResolutions config option to support configurable target resolutions
- Implement ClosestMatch resolution in imagor handler using shared thumbnail package
- Add deprecation warning for DisablePreviews flag at service startup
- Fix parseMaxInputFileSize case sensitivity bug for GB/MB/KB suffixes
- Add unit tests for size parsing and integration tests for imagor endpoint
- Remove redundant sourceSize assignment that always equaled requestedRes
- Replace six-case switch in parseMaxInputFileSize with lookup table
…violations

- Derive Content-Type in S3Cache.Put() using mime.TypeByExtension(key)
  instead of hardcoding application/octet-stream
- Inline credentials.NewStaticV4 calls in test cases per Effective Go
- Add tests for Content-Type derivation (.jpg, .png, unknown extension)
- Add S3 cache env vars to config: BUCKET, REGION, ENDPOINT, ACCESS_KEY, SECRET_KEY
  (WEBDAV_THUMBNAIL_CACHE_S3_* prefix)
- Update NewThumbnailCache factory to accept *S3CacheConfig parameter
- Add 's3' case that creates S3Cache when config is complete, NoopCache otherwise
- Add BuildS3CacheConfig helper to construct S3CacheConfig from raw env var fields
- Log startup warning when imagor + s3 cache configured but S3 config incomplete
- Add unit tests for factory 's3' case and BuildS3CacheConfig
Implement FileCache as a filesystem-based thumbnail cache backend. When
WEBDAV_THUMBNAIL_CACHE_BACKEND=file, thumbnails are stored on disk using
hierarchical directories (checksum[:2]/checksum[2:4]/checksum[4:]/{w}x{h}.{ext})
for filesystem efficiency and atomic writes via temp file + rename pattern.

- FileCache implements ThumbnailCache interface with Get/Put methods
- Hierarchical directory layout mirrors existing thumbnail service storage
- Atomic writes prevent partial files on crash (CreateTemp -> Write -> Sync -> Rename)
- Directory creation with 0700 permissions, parent dirs created as needed
- Put is idempotent — no-op if file already exists for multi-instance safety
- Configured via ThumbnailCacheBackend=file and ThumbnailCacheDir env var
- Default cache directory: /var/lib/opencloud/webdav/thumbnails
- Comprehensive unit tests covering Get/Put/miss, atomic writes, idempotency
- Add resolvePublicLinkAuth to authenticate via gateway with type 'publicshares'
- Handle pre-signed public links with signature+expiration query params
- Build stat path as /public/{token}/{filepath} for public links
- Dispatch PublicThumbnail and PublicThumbnailHead to imagor flow when configured
- Cache keys use 'public-' prefix to avoid collisions with authenticated thumbnails
- Refactor imagorThumbnail/imagorThumbnailPublic into shared imagorThumbnailWithPrefix
- Add tests: successful thumbnail, pre-signed links, password rejection, expired tokens, HEAD checks
- Extract resolveThumbnailBackend for testable backend wiring logic
- Add 8 tests for ThumbnailBackend values (grpc, imagor, none)
- Add 5 tests for env var fallback with semicolon separator pattern
- Add 6 tests for shared extensionInfo map
- Extract shared ext->contentType map from outputExtension/extensionToContentType
- Fix stale config description to include 'file' cache backend
- Return value instead of pointer in getExtensionInfo (safety)
- Add proper doc comments on exported struct fields
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
Add a stateless POST endpoint that accepts raw image bytes via multipart
form and returns resized thumbnail bytes. Mimics imagor's /unsafe/ API
so webdav can use the same client code for both built-in generator and
external imagor instances.

Routes:
  POST /unsafe/fit-in/{W}x{H}/filters:no_upscale()/filters:format({ext})/
  POST /unsafe/{W}x{H}/filters:format({ext})/

Supports jpeg, png, gif output formats. Fit-in mode preserves aspect ratio
with no-upscale behavior. Fill mode crops to exact dimensions.
The push handler now respects the enable_vips build flag like the rest
of the thumbnails service. Common logic (route parsing, encoding, error
handling) lives in push.go. Backend-specific image processing is in:

  push_imaging.go (!enable_vips): imaging.Decode + Fit/Fill
  push_vips.go (enable_vips): vips.NewImageFromBuffer + ThumbnailWithSize

Encoding uses stdlib for all formats since libvips does not support GIF
output. The vips backend converts back to image.Image via PNG export so
stdlib encoders can handle jpg/png/gif uniformly.
Prefactoring for the unified thumbnail architecture. Moves the
preprocessor package from the thumbnails service to webdav where it
will be used by the new ThumbnailWorkflow. Copies mime type and
resolution matching utilities so webdav can validate files
independently.

- Move services/thumbnails/pkg/preprocessor/ → services/webdav/pkg/preprocessor/
  with local error vars (replaces cross-service import)
- Copy SupportedMimeTypes maps + IsMimeTypeSupported to webdav thumbnail pkg
- Copy ParseResolutions + ClosestMatch to webdav thumbnail pkg
- Update two imports in thumbnails service to point to new location

No behavioral changes — same code, different location.
@butonic butonic self-assigned this Aug 24, 2026
@github-project-automation github-project-automation Bot moved this to Qualification in OpenCloud Team Board Aug 24, 2026
@butonic butonic added Type:Maintenance E.g. technical debt, packaging, etc. Type:Enhancement labels Aug 24, 2026
@butonic butonic moved this from Qualification to In Progress in OpenCloud Team Board Aug 24, 2026
@codacy-production

codacy-production Bot commented Aug 24, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 5 critical · 6 minor

Alerts:
⚠ 11 issues (≤ 0 issues of at least minor severity)

Results:
11 new issues

Category Results
Security 5 critical
CodeStyle 6 minor

View in Codacy

🟢 Metrics 515 complexity · 121 duplication

Metric Results
Complexity 515
Duplication 121

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Rewrites webdav's thumbnail handling with a single ThumbnailWorkflow
type that owns the complete pipeline (stat → validate → cache check →
download → generate → cache → respond). This eliminates the strategy
pattern and the backend enum config.

- Rename pkg/imagor/ → pkg/generator/ (PostToImagor→PostImage, BuildImagorURL→BuildURL)
- Create ThumbnailWorkflow in pkg/thumbnail/workflow/ with injected
  dependencies (Stater, FileDownloader, cache, *http.Client)
- Delete Thumbnailer interface + imagorThumbnailer + grpcThumbnailer
- New config: ThumbnailGeneratorURL, ThumbnailGeneratorTimeout,
  ThumbnailGeneratorAuthHeader, MaxInputFileSize
- Remove old config: DisablePreviews, ThumbnailBackend, ImagorURL,
  ImagorTimeout, ImagorMaxInputFileSize
- All four handlers delegate to workflow.Execute/ExecutePublic/Head
- Integration tests moved to workflow package (12 specs)
After webdav took over the complete thumbnail workflow (slices 01-03),
the thumbnails service is now a stateless image resizer. Delete all
pull-based gRPC path code that is no longer used.

Deleted:
- Proto files + generated Go code (thumbnails service + messages)
- gRPC service handler (pkg/service/grpc/) with decorators
- JWT transfer token code (pkg/service/jwt/)
- Storage layer (pkg/thumbnail/storage/) - filesystem storage, keys
- Image source fetching (pkg/thumbnail/imgsource/) - webdav + CS3
- Duplicated utilities (encoding, generators, processor, resolutions)
- /data HTTP endpoint + TransferTokenValidator middleware
- gRPC server (pkg/server/grpc/)

Kept:
- Push endpoint (push.go, push_imaging.go, push_vips.go)
- Minimal SupportedMimeTypes map (still imported by reva dependency)
- HTTP server, debug server, config, metrics

Repointed webdav search + graph sharedbyme/sharedwithme to use
webdav's own copy of SupportedMimeTypes.
Thumbnails service is now a stateless image resizer; remove all leftover
code from the old pull-based architecture:

- Delete pkg/errors (error types belonged to deleted generators/imgsource)
- Delete pkg/config/grpc.go, GRPC config field, debug grpc check
- Delete dead Thumbnail config fields + FileSystemStorage type
- Delete unused GRPCClientTLS/GrpcClient and go-micro client creation
- Delete HTTP.Root (never read)
- Reduce metrics to BuildInfo only; delete no-op instrument/logging wrappers
- Remove unused Thumbnails.config field from HTTP service
- Delete testdata files, Makefile protobuf target
- Rewrite README for the stateless push endpoint

Webdav:
- Remove unused Webdav.cache struct field
- Remove dead errTooEarly/errTooManyRequests/addRetryAfterHeader helpers
- Remove dead generator.PostImage duplicate (workflow has its own)

Opencloud init:
- Remove thumbnails transfer_secret generation and config section
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type:Enhancement Type:Maintenance E.g. technical debt, packaging, etc.

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Thumbnailer should use a push based mechanism

1 participant