diff --git a/services/thumbnails/pkg/config/config.go b/services/thumbnails/pkg/config/config.go index 6d39e03900..639ca1644a 100644 --- a/services/thumbnails/pkg/config/config.go +++ b/services/thumbnails/pkg/config/config.go @@ -36,5 +36,5 @@ type Thumbnail struct { // decoded and resized in parallel. 0 (the default) means unlimited, matching // main's THUMBNAILS_MAX_CONCURRENT_REQUESTS. Requests arriving while the limit // is reached get HTTP 429 Too Many Requests. - MaxConcurrentRequests int `yaml:"max_concurrent_requests" env:"THUMBNAILS_MAX_CONCURRENT_REQUESTS" desc:"Maximum number of concurrent thumbnail generation requests. Default is 0 which is unlimited." introductionVersion:"1.0.0"` + MaxConcurrentRequests int `yaml:"max_concurrent_requests" env:"THUMBNAILS_MAX_CONCURRENT_REQUESTS" desc:"Maximum number of concurrent thumbnail generation requests. Default is 0 which is unlimited." introductionVersion:"%%NEXT%%"` } diff --git a/services/webdav/pkg/config/config.go b/services/webdav/pkg/config/config.go index b00b1311a9..0c0e5c1645 100644 --- a/services/webdav/pkg/config/config.go +++ b/services/webdav/pkg/config/config.go @@ -24,20 +24,23 @@ type Config struct { OpenCloudPublicURL string `yaml:"opencloud_public_url" env:"OC_URL;OC_PUBLIC_URL" desc:"URL, where OpenCloud is reachable for users." introductionVersion:"1.0.0"` WebdavNamespace string `yaml:"webdav_namespace" env:"WEBDAV_WEBDAV_NAMESPACE" desc:"CS3 path layout to use when forwarding /webdav requests" introductionVersion:"1.0.0"` RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"1.0.0"` + DisablePreviews bool `yaml:"disablePreviews" env:"OC_DISABLE_PREVIEWS;WEBDAV_DISABLE_PREVIEWS" desc:"Set this option to 'true' to disable rendering of thumbnails triggered via webdav access. Note that when disabled, all access to preview related webdav paths will return a 404." introductionVersion:"1.0.0"` - ThumbnailGeneratorURL string `yaml:"thumbnail_generator_url" env:"WEBDAV_THUMBNAIL_GENERATOR_URL" desc:"Base URL of the thumbnail generator service, e.g. http://thumbnails:9130" introductionVersion:"1.0.0"` - ThumbnailGeneratorTimeout string `yaml:"thumbnail_generator_timeout" env:"WEBDAV_THUMBNAIL_GENERATOR_TIMEOUT" desc:"HTTP timeout for requests to the thumbnail generator, e.g. 30s" introductionVersion:"1.0.0"` - MaxInputFileSize string `yaml:"max_input_file_size" env:"WEBDAV_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE;THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE" desc:"Maximum file size of an input image for thumbnail generation. Usable common abbreviations: [KB, KiB, MB, MiB, GB, GiB], example: 50MB" introductionVersion:"1.0.0"` + ThumbnailGeneratorURL string `yaml:"thumbnail_generator_url" env:"WEBDAV_THUMBNAIL_GENERATOR_URL" desc:"Base URL of the thumbnail generator service, e.g. http://thumbnails:9130" introductionVersion:"%%NEXT%%"` + ThumbnailGeneratorTimeout string `yaml:"thumbnail_generator_timeout" env:"WEBDAV_THUMBNAIL_GENERATOR_TIMEOUT" desc:"HTTP timeout for requests to the thumbnail generator, e.g. 30s" introductionVersion:"%%NEXT%%"` + // the yaml key stays unprefixed, a config file written against it has to keep working + ThumbnailMaxInputFileSize string `yaml:"max_input_file_size" env:"WEBDAV_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE;THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE" desc:"Maximum file size of an input image for thumbnail generation. Usable common abbreviations: [KB, KiB, MB, MiB, GB, GiB], example: 50MB" introductionVersion:"%%NEXT%%"` - ThumbnailCacheBackend string `yaml:"thumbnail_cache_backend" env:"WEBDAV_THUMBNAIL_CACHE_BACKEND" desc:"Cache backend for imagor thumbnails: 'none', 'memory', 'file', or 's3'. Default: file." introductionVersion:"1.0.0"` - ThumbnailCacheDir string `yaml:"thumbnail_cache_dir" env:"WEBDAV_THUMBNAIL_CACHE_DIR" desc:"Directory for file-based thumbnail cache (Default: $OC_BASE_DATA_PATH/thumbnails/files). Only used when ThumbnailCacheBackend is 'file'." introductionVersion:"1.0.0"` - ThumbnailResolutions []string `yaml:"thumbnail_resolutions" env:"THUMBNAILS_RESOLUTIONS;WEBDAV_THUMBNAIL_RESOLUTIONS" desc:"Allowed thumbnail edge lengths, like 512. Each axis of a requested size is rounded up to the next one of these before the box is sent to the generator. Legacy WidthxHeight entries are still accepted and contribute both of their sides." introductionVersion:"1.0.0"` - ThumbnailCacheS3Bucket string `yaml:"thumbnail_cache_s3_bucket" env:"WEBDAV_THUMBNAIL_CACHE_S3_BUCKET" desc:"S3 bucket name for thumbnail cache when ThumbnailCacheBackend is 's3'." introductionVersion:"1.0.0"` - ThumbnailCacheS3Region string `yaml:"thumbnail_cache_s3_region" env:"WEBDAV_THUMBNAIL_CACHE_S3_REGION" desc:"S3 region for thumbnail cache when ThumbnailCacheBackend is 's3'." introductionVersion:"1.0.0"` - 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:"1.0.0"` - ThumbnailCacheS3AccessKey string `yaml:"thumbnail_cache_s3_access_key" env:"WEBDAV_THUMBNAIL_CACHE_S3_ACCESS_KEY" desc:"S3 access key for thumbnail cache authentication." introductionVersion:"1.0.0"` - ThumbnailCacheS3SecretKey string `yaml:"thumbnail_cache_s3_secret_key" env:"WEBDAV_THUMBNAIL_CACHE_S3_SECRET_KEY" desc:"S3 secret key for thumbnail cache authentication." introductionVersion:"1.0.0"` - FontMapFile string `yaml:"font_map_file" env:"WEBDAV_THUMBNAILS_TXT_FONTMAP_FILE;THUMBNAILS_TXT_FONTMAP_FILE" desc:"The path to a font map file for txt thumbnails." introductionVersion:"1.0.0"` + ThumbnailCacheBackend string `yaml:"thumbnail_cache_backend" env:"WEBDAV_THUMBNAIL_CACHE_BACKEND" desc:"Cache backend for imagor thumbnails: 'none', 'memory', 'file', or 's3'. Default: file." introductionVersion:"%%NEXT%%"` + ThumbnailCacheDir string `yaml:"thumbnail_cache_dir" env:"WEBDAV_THUMBNAIL_CACHE_DIR" desc:"Directory for file-based thumbnail cache (Default: $OC_BASE_DATA_PATH/thumbnails/files). Only used when ThumbnailCacheBackend is 'file'." introductionVersion:"%%NEXT%%"` + ThumbnailResolutions []string `yaml:"thumbnail_resolutions" env:"THUMBNAILS_RESOLUTIONS;WEBDAV_THUMBNAIL_RESOLUTIONS" desc:"Allowed thumbnail edge lengths, like 512. Each axis of a requested size is rounded up to the next one of these before the box is sent to the generator. Legacy WidthxHeight entries are still accepted and contribute both of their sides." introductionVersion:"%%NEXT%%"` + ThumbnailCacheS3Bucket string `yaml:"thumbnail_cache_s3_bucket" env:"WEBDAV_THUMBNAIL_CACHE_S3_BUCKET" desc:"S3 bucket name for thumbnail cache when ThumbnailCacheBackend is 's3'." introductionVersion:"%%NEXT%%"` + ThumbnailCacheS3Region string `yaml:"thumbnail_cache_s3_region" env:"WEBDAV_THUMBNAIL_CACHE_S3_REGION" desc:"S3 region for thumbnail cache when ThumbnailCacheBackend is 's3'." introductionVersion:"%%NEXT%%"` + 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%%"` + // the yaml key stays unprefixed, a config file written against it has to keep working + ThumbnailFontMapFile string `yaml:"font_map_file" env:"WEBDAV_THUMBNAILS_TXT_FONTMAP_FILE;THUMBNAILS_TXT_FONTMAP_FILE" desc:"The path to a font map file for txt thumbnails." introductionVersion:"%%NEXT%%"` Context context.Context `yaml:"-"` } diff --git a/services/webdav/pkg/config/defaults/defaultconfig.go b/services/webdav/pkg/config/defaults/defaultconfig.go index f083f48df1..962b24522d 100644 --- a/services/webdav/pkg/config/defaults/defaultconfig.go +++ b/services/webdav/pkg/config/defaults/defaultconfig.go @@ -47,7 +47,7 @@ func DefaultConfig() *config.Config { ThumbnailGeneratorURL: "http://127.0.0.1:9186", ThumbnailGeneratorTimeout: "30s", - MaxInputFileSize: "50MB", + ThumbnailMaxInputFileSize: "50MB", ThumbnailCacheBackend: "file", ThumbnailCacheDir: path.Join(cored.BaseDataPath(), "thumbnails", "files"), ThumbnailResolutions: []string{ diff --git a/services/webdav/pkg/service/v0/search.go b/services/webdav/pkg/service/v0/search.go index 1b68562db4..59739317f7 100644 --- a/services/webdav/pkg/service/v0/search.go +++ b/services/webdav/pkg/service/v0/search.go @@ -26,10 +26,10 @@ import ( searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" "github.com/opencloud-eu/opencloud/services/webdav/pkg/constants" - "github.com/opencloud-eu/opencloud/services/webdav/pkg/thumbnail" "github.com/opencloud-eu/opencloud/services/webdav/pkg/net" "github.com/opencloud-eu/opencloud/services/webdav/pkg/prop" "github.com/opencloud-eu/opencloud/services/webdav/pkg/propfind" + "github.com/opencloud-eu/opencloud/services/webdav/pkg/thumbnail" ) const ( diff --git a/services/webdav/pkg/service/v0/service.go b/services/webdav/pkg/service/v0/service.go index 95d5f78c32..5b5acf8004 100644 --- a/services/webdav/pkg/service/v0/service.go +++ b/services/webdav/pkg/service/v0/service.go @@ -84,6 +84,83 @@ func NewService(opts ...Option) (Service, error) { return nil, err } + // with previews disabled nothing talks to the generator, so the workflow + // and its cache are never built + var wf *workflow.ThumbnailWorkflow + if !conf.DisablePreviews { + wf, err = newThumbnailWorkflow(conf, options.Logger, gatewaySelector) + if err != nil { + return nil, fmt.Errorf("create thumbnail workflow: %w", err) + } + } + + svc := Webdav{ + config: conf, + log: options.Logger, + mux: m, + searchClient: searchsvc.NewSearchProviderService("eu.opencloud.api.search", conf.GrpcClient), + workflow: wf, + gatewaySelector: gatewaySelector, + } + + m.Route(options.Config.HTTP.Root, func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(svc.DavUserContext()) + + if !conf.DisablePreviews { + r.Get("/remote.php/dav/spaces/{id}", svc.SpacesThumbnail) + r.Get("/remote.php/dav/spaces/{id}/*", svc.SpacesThumbnail) + r.Get("/dav/spaces/{id}", svc.SpacesThumbnail) + r.Get("/dav/spaces/{id}/*", svc.SpacesThumbnail) + + r.Get("/remote.php/dav/files/{id}", svc.Thumbnail) + r.Get("/remote.php/dav/files/{id}/*", svc.Thumbnail) + r.Get("/dav/files/{id}", svc.Thumbnail) + r.Get("/dav/files/{id}/*", svc.Thumbnail) + } + + r.MethodFunc("REPORT", "/remote.php/dav/spaces*", svc.Search) + r.MethodFunc("REPORT", "/dav/spaces*", svc.Search) + r.MethodFunc("REPORT", "/remote.php/dav/files*", svc.Search) + r.MethodFunc("REPORT", "/dav/files*", svc.Search) + }) + + if !conf.DisablePreviews { + r.Group(func(r chi.Router) { + r.Use(svc.DavPublicContext()) + + r.Head("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnailHead) + r.Head("/dav/public-files/{token}/*", svc.PublicThumbnailHead) + + r.Get("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnail) + r.Get("/dav/public-files/{token}/*", svc.PublicThumbnail) + }) + } + + r.Group(func(r chi.Router) { + r.Use(svc.WebDAVContext()) + + if !conf.DisablePreviews { + r.Get("/remote.php/webdav/*", svc.Thumbnail) + r.Get("/webdav/*", svc.Thumbnail) + } + + r.MethodFunc("REPORT", "/remote.php/webdav*", svc.Search) + r.MethodFunc("REPORT", "/webdav*", svc.Search) + }) + }) + + _ = chi.Walk(m, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") + return nil + }) + + return svc, nil +} + +// newThumbnailWorkflow wires the preview pipeline: the generator client, the +// cache and the gateway-backed lookups. +func newThumbnailWorkflow(conf *config.Config, logger log.Logger, gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]) (*workflow.ThumbnailWorkflow, error) { genTimeout, _ := time.ParseDuration(conf.ThumbnailGeneratorTimeout) if genTimeout == 0 { genTimeout = 30 * time.Second @@ -94,7 +171,7 @@ func NewService(opts ...Option) (Service, error) { return nil, fmt.Errorf("parse thumbnail sizes: %w", err) } - maxInputSize, err := generator.ParseMaxInputFileSize(conf.MaxInputFileSize) + maxInputSize, err := generator.ParseMaxInputFileSize(conf.ThumbnailMaxInputFileSize) if err != nil { return nil, fmt.Errorf("parse max input file size: %w", err) } @@ -111,79 +188,20 @@ func NewService(opts ...Option) (Service, error) { httpClient := &http.Client{Timeout: genTimeout} - wf, err := workflow.NewWorkflow( + return workflow.NewWorkflow( workflow.WithGeneratorURL(conf.ThumbnailGeneratorURL), workflow.WithCache(c), workflow.WithHTTPClient(httpClient), workflow.WithMaxInputSize(maxInputSize), workflow.WithSizes(sizes), workflow.WithWebdavNamespace(conf.WebdavNamespace), - workflow.WithFontMapFile(conf.FontMapFile), - workflow.WithLogger(options.Logger), + workflow.WithFontMapFile(conf.ThumbnailFontMapFile), + workflow.WithLogger(logger), workflow.WithStater(workflow.NewGatewayStater(gatewaySelector)), workflow.WithFileDownloader(workflow.NewGatewayFileDownloader(gatewaySelector, httpClient)), workflow.WithSpaceLookup(workflow.NewGatewaySpaceLookup(gatewaySelector)), workflow.WithUserResolver(workflow.NewGatewayUserResolver(gatewaySelector)), ) - if err != nil { - return nil, fmt.Errorf("create thumbnail workflow: %w", err) - } - - svc := Webdav{ - config: conf, - log: options.Logger, - mux: m, - searchClient: searchsvc.NewSearchProviderService("eu.opencloud.api.search", conf.GrpcClient), - workflow: wf, - gatewaySelector: gatewaySelector, - } - - m.Route(options.Config.HTTP.Root, func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(svc.DavUserContext()) - - r.Get("/remote.php/dav/spaces/{id}", svc.SpacesThumbnail) - r.Get("/remote.php/dav/spaces/{id}/*", svc.SpacesThumbnail) - r.Get("/dav/spaces/{id}", svc.SpacesThumbnail) - r.Get("/dav/spaces/{id}/*", svc.SpacesThumbnail) - r.MethodFunc("REPORT", "/remote.php/dav/spaces*", svc.Search) - r.MethodFunc("REPORT", "/dav/spaces*", svc.Search) - - r.Get("/remote.php/dav/files/{id}", svc.Thumbnail) - r.Get("/remote.php/dav/files/{id}/*", svc.Thumbnail) - r.Get("/dav/files/{id}", svc.Thumbnail) - r.Get("/dav/files/{id}/*", svc.Thumbnail) - - r.MethodFunc("REPORT", "/remote.php/dav/files*", svc.Search) - r.MethodFunc("REPORT", "/dav/files*", svc.Search) - }) - - r.Group(func(r chi.Router) { - r.Use(svc.DavPublicContext()) - - r.Head("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnailHead) - r.Head("/dav/public-files/{token}/*", svc.PublicThumbnailHead) - - r.Get("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnail) - r.Get("/dav/public-files/{token}/*", svc.PublicThumbnail) - }) - - r.Group(func(r chi.Router) { - r.Use(svc.WebDAVContext()) - r.Get("/remote.php/webdav/*", svc.Thumbnail) - r.Get("/webdav/*", svc.Thumbnail) - - r.MethodFunc("REPORT", "/remote.php/webdav*", svc.Search) - r.MethodFunc("REPORT", "/webdav*", svc.Search) - }) - }) - - _ = chi.Walk(m, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { - options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") - return nil - }) - - return svc, nil } // Webdav implements the business logic for Service.