diff --git a/services/web/pkg/config/config.go b/services/web/pkg/config/config.go index aec38353b3..af102f8a1e 100644 --- a/services/web/pkg/config/config.go +++ b/services/web/pkg/config/config.go @@ -32,6 +32,7 @@ type Config struct { type Asset struct { CorePath string `yaml:"core_path" env:"WEB_ASSET_CORE_PATH" desc:"Serve OpenCloud Web assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OC_BASE_DATA_PATH/web/assets/core" introductionVersion:"1.0.0"` ThemesPath string `yaml:"themes_path" env:"OC_ASSET_THEMES_PATH;WEB_ASSET_THEMES_PATH" desc:"Serve OpenCloud themes from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OC_BASE_DATA_PATH/web/assets/themes" introductionVersion:"1.0.0"` + FontsPath string `yaml:"fonts_path" env:"WEB_ASSET_FONTS_PATH" desc:"Serve fonts from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OC_BASE_DATA_PATH/web/assets/fonts" introductionVersion:"%%NEXT%%"` AppsPath string `yaml:"apps_path" env:"WEB_ASSET_APPS_PATH" desc:"Serve OpenCloud Web apps assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OC_BASE_DATA_PATH/web/assets/apps" introductionVersion:"1.0.0"` } diff --git a/services/web/pkg/config/defaults/defaultconfig.go b/services/web/pkg/config/defaults/defaultconfig.go index 8120394aa6..2f8ba3990a 100644 --- a/services/web/pkg/config/defaults/defaultconfig.go +++ b/services/web/pkg/config/defaults/defaultconfig.go @@ -83,6 +83,7 @@ func DefaultConfig() *config.Config { CorePath: filepath.Join(defaults.BaseDataPath(), "web/assets/core"), AppsPath: filepath.Join(defaults.BaseDataPath(), "web/assets/apps"), ThemesPath: filepath.Join(defaults.BaseDataPath(), "web/assets/themes"), + FontsPath: filepath.Join(defaults.BaseDataPath(), "web/assets/fonts"), }, GatewayAddress: "eu.opencloud.api.gateway", Web: config.Web{ diff --git a/services/web/pkg/server/http/server.go b/services/web/pkg/server/http/server.go index 25f210b13b..8190b6ca0f 100644 --- a/services/web/pkg/server/http/server.go +++ b/services/web/pkg/server/http/server.go @@ -75,12 +75,17 @@ func Server(opts ...Option) (http.Service, error) { fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.ThemesPath), fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes"), ) + fontFS := fsx.NewFallbackFS( + fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.FontsPath), + fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/fonts"), + ) handle, err := svc.NewService( svc.Logger(options.Logger), svc.CoreFS(coreFS.IOFS()), svc.AppFS(appsFS.IOFS()), svc.ThemeFS(themeFS), + svc.FontFS(fontFS.IOFS()), svc.AppsHTTPEndpoint(_customAppsEndpoint), svc.Config(options.Config), svc.GatewaySelector(gatewaySelector), diff --git a/services/web/pkg/service/v0/option.go b/services/web/pkg/service/v0/option.go index 033c139bf7..f784e7b1b2 100644 --- a/services/web/pkg/service/v0/option.go +++ b/services/web/pkg/service/v0/option.go @@ -26,6 +26,7 @@ type Options struct { AppsHTTPEndpoint string CoreFS fs.FS AppFS fs.FS + FontFS fs.FS ThemeFS *fsx.FallbackFS } @@ -82,6 +83,13 @@ func AppFS(val fs.FS) Option { } } +// FontFS provides a function to set the fontFS option. +func FontFS(val fs.FS) Option { + return func(o *Options) { + o.FontFS = val + } +} + // ThemeFS provides a function to set the themeFS option. func ThemeFS(val *fsx.FallbackFS) Option { return func(o *Options) { diff --git a/services/web/pkg/service/v0/service.go b/services/web/pkg/service/v0/service.go index a16ba5336f..4f136da4a5 100644 --- a/services/web/pkg/service/v0/service.go +++ b/services/web/pkg/service/v0/service.go @@ -2,6 +2,7 @@ package svc import ( "encoding/json" + "io" "io/fs" "net/http" "net/url" @@ -14,12 +15,12 @@ import ( "github.com/go-chi/chi/v5" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/riandyrn/otelchi" + "golang.org/x/image/font/sfnt" "github.com/opencloud-eu/opencloud/pkg/account" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/middleware" "github.com/opencloud-eu/opencloud/pkg/tracing" - "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" "github.com/opencloud-eu/opencloud/services/web/pkg/assets" "github.com/opencloud-eu/opencloud/services/web/pkg/config" "github.com/opencloud-eu/opencloud/services/web/pkg/theme" @@ -54,8 +55,6 @@ func NewService(opts ...Option) (Service, error) { logger: options.Logger, config: options.Config, mux: m, - coreFS: options.CoreFS, - themeFS: options.ThemeFS, gatewaySelector: options.GatewaySelector, } @@ -78,6 +77,98 @@ func NewService(opts ...Option) (Service, error) { r.Post("/", themeService.LogoUpload) r.Delete("/", themeService.LogoReset) }) + r.Route("/fonts", func(r chi.Router) { + r.Get("/fonts.json", func(w http.ResponseWriter, r *http.Request) { + fontFiles, err := fs.ReadDir(options.FontFS, ".") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fontMap := []map[string]any{} + for _, fontFile := range fontFiles { + if fontFile.IsDir() { + continue + } + + fLogger := options.Logger.Info().Str("font", fontFile.Name()) + + uri, err := url.JoinPath(options.Config.Web.Config.Server, svc.config.HTTP.Root, "/fonts", path.Base(fontFile.Name())) + if err != nil { + fLogger.Err(err).Msg("could not build font uri") + continue + } + + f, err := options.FontFS.Open(fontFile.Name()) + if err != nil { + fLogger.Err(err).Msg("could not open font file") + continue + } + + b, err := io.ReadAll(f) + switch { + case err != nil: + fLogger.Err(err).Msg("could not read font file") + continue + case len(b) == 0: + fLogger.Msg("font file is empty") + continue + } + + font, err := sfnt.Parse(b) + if err != nil { + fLogger.Err(err).Msg("could not parse font file") + continue + } + + buf := new(sfnt.Buffer) + nameID := func(id sfnt.NameID) string { + name, err := font.Name(buf, id) + if err != nil { + fLogger.Err(err).Msg("could not extract font details") + } + + return name + } + + fontMap = append(fontMap, map[string]any{ + "copyright": nameID(sfnt.NameIDCopyright), + "family": nameID(sfnt.NameIDFamily), + "version": nameID(sfnt.NameIDVersion), + "trademark": nameID(sfnt.NameIDTrademark), + "manufacturer": nameID(sfnt.NameIDManufacturer), + "designer": nameID(sfnt.NameIDDesigner), + "description": nameID(sfnt.NameIDDescription), + "vendor_url": nameID(sfnt.NameIDVendorURL), + "designer_url": nameID(sfnt.NameIDDesignerURL), + "license": nameID(sfnt.NameIDLicense), + "license_url": nameID(sfnt.NameIDLicenseURL), + "uri": uri, + }) + } + + b, err := json.Marshal(map[string]any{ + "kind": "fontconfiguration", + "server": "OpenCloud Fonts", + "fonts": fontMap, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, err = w.Write(b) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + r.Mount("/", svc.Static( + options.FontFS, + path.Join(svc.config.HTTP.Root, "/fonts"), + options.Config.HTTP.CacheTTL, + )) + }) r.Route("/themes", func(r chi.Router) { r.Get("/{id}/theme.json", themeService.Get) r.Mount("/", svc.Static( @@ -92,7 +183,7 @@ func NewService(opts ...Option) (Service, error) { options.Config.HTTP.CacheTTL, )) r.Mount("/", svc.Static( - svc.coreFS, + options.CoreFS, svc.config.HTTP.Root, options.Config.HTTP.CacheTTL, )) @@ -110,8 +201,6 @@ type Web struct { logger log.Logger config *config.Config mux *chi.Mux - coreFS fs.FS - themeFS *fsx.FallbackFS gatewaySelector pool.Selectable[gateway.GatewayAPIClient] }