Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions services/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ Note that clients will respond with a connection error if the web service is not

The web service also provides a minimal API for branding functionality like changing the logo shown.

## Static-only Mode

Set the environment variable `WEB_STATIC_ONLY=true` to disable the dynamic web API surface (branding endpoints, theme handlers, external app discovery) and serve the embedded/static assets only. Ensure that a `config.json` file exists in the asset root (`services/web/assets/core/config.json`) or provide one via `WEB_UI_CONFIG_FILE`; this file is exposed through `/config.json` by the static file server.

### Development (local binary)

```bash
go build -o bin/web ./services/web/cmd/web

# Option A: point to a config file without modifying the assets tree
WEB_STATIC_ONLY=true \
WEB_HTTP_ADDR=127.0.0.1:9100 \
WEB_UI_CONFIG_FILE=$(pwd)/services/web/assets/core/config.json \
./bin/web

# Option B: place config.json inside services/web/assets/core and rely on the assets only
WEB_STATIC_ONLY=true \
WEB_HTTP_ADDR=127.0.0.1:9100 \
./bin/web

# Verification
curl -sS http://127.0.0.1:9100/config.json | jq . >/dev/null
curl -I http://127.0.0.1:9100/
```

### Production (existing Docker image)

```bash
make -C ocis dev-docker
docker run --rm -p 9100:9100 \
-e WEB_STATIC_ONLY=true \
-e WEB_HTTP_ADDR=0.0.0.0:9100 \
-e WEB_ASSET_CORE_PATH=/data/assets/core \
-v $(pwd)/services/web/assets/core:/data/assets/core:ro \
--entrypoint web \
owncloud/ocis:dev

# Optional: mount a standalone config.json file instead of updating the assets directory
# -v $(pwd)/services/web/assets/core/config.json:/data/config.json:ro \
# -e WEB_UI_CONFIG_FILE=/data/config.json \

# Verification
curl -sS http://localhost:9100/config.json | jq . >/dev/null
curl -I http://localhost:9100/
```

## Custom Compiled Web Assets

If you want to use your custom compiled web client assets instead of the embedded ones,
Expand Down
1 change: 1 addition & 0 deletions services/web/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type ExternalAppConfig struct {
type Web struct {
ThemeServer string `yaml:"theme_server" env:"OCIS_URL;WEB_UI_THEME_SERVER" desc:"Base URL to load themes from. Will be prepended to the theme path." introductionVersion:"pre5.0"` // used to build Theme in WebConfig
ThemePath string `yaml:"theme_path" env:"WEB_UI_THEME_PATH" desc:"Path to the theme json file. Will be appended to the URL of the theme server." introductionVersion:"pre5.0"` // used to build Theme in WebConfig
StaticOnly bool `yaml:"static_only" env:"WEB_STATIC_ONLY" desc:"Serve only static assets without exposing dynamic web endpoints. Requires a config.json file in the asset root or provided via WEB_UI_CONFIG_FILE."`
Config WebConfig `yaml:"config"`
}

Expand Down
54 changes: 39 additions & 15 deletions services/web/pkg/server/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package http

import (
"fmt"
"os"
"path"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
"go-micro.dev/v4"
Expand All @@ -17,6 +19,7 @@ import (
"github.com/owncloud/ocis/v2/services/web"
"github.com/owncloud/ocis/v2/services/web/pkg/apps"
svc "github.com/owncloud/ocis/v2/services/web/pkg/service/v0"
"github.com/spf13/afero"
)

var (
Expand Down Expand Up @@ -46,31 +49,48 @@ func Server(opts ...Option) (http.Service, error) {
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
}

gatewaySelector, err := pool.GatewaySelector(
options.Config.GatewayAddress,
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(options.TraceProvider),
)
if err != nil {
return http.Service{}, err
var gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
if !options.Config.Web.StaticOnly {
var err error
gatewaySelector, err = pool.GatewaySelector(
options.Config.GatewayAddress,
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(options.TraceProvider),
)
if err != nil {
return http.Service{}, err
}
}

appsFS := fsx.NewFallbackFS(
fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.AppsPath)),
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/apps"),
)
// build and inject the list of applications into the config
for _, application := range apps.List(options.Logger, options.Config.Apps, appsFS.Secondary().IOFS(), appsFS.Primary().IOFS()) {
options.Config.Web.Config.ExternalApps = append(
options.Config.Web.Config.ExternalApps,
application.ToExternal(path.Join(options.Config.HTTP.Root, _customAppsEndpoint)),
)
if !options.Config.Web.StaticOnly {
for _, application := range apps.List(options.Logger, options.Config.Apps, appsFS.Secondary().IOFS(), appsFS.Primary().IOFS()) {
options.Config.Web.Config.ExternalApps = append(
options.Config.Web.Config.ExternalApps,
application.ToExternal(path.Join(options.Config.HTTP.Root, _customAppsEndpoint)),
)
}
}

coreFS := fsx.NewFallbackFS(
fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.CorePath),
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/core"),
)
if options.Config.Web.StaticOnly && options.Config.File != "" {
data, err := os.ReadFile(options.Config.File)
if err != nil {
return http.Service{}, fmt.Errorf("load static config: %w", err)
}
mem := fsx.NewMemMapFs()
if err := afero.WriteFile(mem, "config.json", data, 0o644); err != nil {
return http.Service{}, fmt.Errorf("write static config: %w", err)
}
coreFS = fsx.NewFallbackFS(mem, coreFS)
}
themeFS := fsx.NewFallbackFS(
fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.ThemesPath),
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes"),
Expand All @@ -87,14 +107,13 @@ func Server(opts ...Option) (http.Service, error) {
fsx.NewBasePathFs(coreFS.Secondary(), "themes"),
)

handle, err := svc.NewService(
svcOptions := []svc.Option{
svc.Logger(options.Logger),
svc.CoreFS(coreFS.IOFS()),
svc.AppFS(appsFS.IOFS()),
svc.ThemeFS(themeFS),
svc.AppsHTTPEndpoint(_customAppsEndpoint),
svc.Config(options.Config),
svc.GatewaySelector(gatewaySelector),
svc.Middleware(
middleware.GetOtelhttpMiddleware(options.Config.Service.Name, options.TraceProvider),
chimiddleware.RealIP,
Expand All @@ -117,7 +136,12 @@ func Server(opts ...Option) (http.Service, error) {
),
),
svc.TraceProvider(options.TraceProvider),
)
}
if !options.Config.Web.StaticOnly {
svcOptions = append(svcOptions, svc.GatewaySelector(gatewaySelector))
}

handle, err := svc.NewService(svcOptions...)

if err != nil {
return http.Service{}, err
Expand Down
61 changes: 34 additions & 27 deletions services/web/pkg/service/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,46 @@ func NewService(opts ...Option) (Service, error) {
themeFS: options.ThemeFS,
gatewaySelector: options.GatewaySelector,
}

themeService, err := theme.NewService(
theme.ServiceOptions{}.
WithThemeFS(options.ThemeFS).
WithGatewaySelector(options.GatewaySelector),
var (
err error
themeService theme.Service
)
if err != nil {
return svc, err
if !options.Config.Web.StaticOnly {
themeService, err = theme.NewService(
theme.ServiceOptions{}.
WithThemeFS(options.ThemeFS).
WithGatewaySelector(options.GatewaySelector),
)
if err != nil {
return svc, err
}
}

m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.Get("/config.json", svc.Config)
r.Route("/branding/logo", func(r chi.Router) {
r.Use(middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
))
r.Post("/", themeService.LogoUpload)
r.Delete("/", themeService.LogoReset)
})
r.Route("/themes", func(r chi.Router) {
r.Get("/{id}/theme.json", themeService.Get)
r.Mount("/", svc.Static(
options.ThemeFS.IOFS(),
path.Join(svc.config.HTTP.Root, "/themes"),
if !options.Config.Web.StaticOnly {
r.Get("/config.json", svc.Config)
r.Route("/branding/logo", func(r chi.Router) {
r.Use(middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
))
r.Post("/", themeService.LogoUpload)
r.Delete("/", themeService.LogoReset)
})
r.Route("/themes", func(r chi.Router) {
r.Get("/{id}/theme.json", themeService.Get)
r.Mount("/", svc.Static(
options.ThemeFS.IOFS(),
path.Join(svc.config.HTTP.Root, "/themes"),
options.Config.HTTP.CacheTTL,
))
})
r.Mount(options.AppsHTTPEndpoint, svc.Static(
options.AppFS,
path.Join(svc.config.HTTP.Root, options.AppsHTTPEndpoint),
options.Config.HTTP.CacheTTL,
))
})
r.Mount(options.AppsHTTPEndpoint, svc.Static(
options.AppFS,
path.Join(svc.config.HTTP.Root, options.AppsHTTPEndpoint),
options.Config.HTTP.CacheTTL,
))
}
r.Mount("/", svc.Static(
svc.coreFS,
svc.config.HTTP.Root,
Expand Down