refactor webdav
Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!bin/
|
||||
@@ -0,0 +1,37 @@
|
||||
SHELL := bash
|
||||
NAME := webdav
|
||||
|
||||
include ../../.make/recursion.mk
|
||||
|
||||
############ tooling ############
|
||||
ifneq (, $(shell which go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
|
||||
include ../../.bingo/Variables.mk
|
||||
endif
|
||||
|
||||
############ go tooling ############
|
||||
include ../../.make/go.mk
|
||||
|
||||
############ release #########
|
||||
include ../../.make/release.mk
|
||||
|
||||
############ docs generate ############
|
||||
include ../../.make/docs.mk
|
||||
|
||||
.PHONY: docs-generate
|
||||
docs-generate: config-docs-generate
|
||||
|
||||
############ generate ############
|
||||
include ../../.make/generate.mk
|
||||
|
||||
.PHONY: ci-go-generate
|
||||
ci-go-generate: # CI runs ci-node-generate automatically before this target
|
||||
|
||||
.PHONY: ci-node-generate
|
||||
ci-node-generate:
|
||||
|
||||
############ licenses ############
|
||||
.PHONY: ci-node-check-licenses
|
||||
ci-node-check-licenses:
|
||||
|
||||
.PHONY: ci-node-save-licenses
|
||||
ci-node-save-licenses:
|
||||
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/command"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config/defaults"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.Execute(defaults.DefaultConfig()); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!example.json
|
||||
!example.yml
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM amd64/alpine:latest
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add ca-certificates mailcap && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
echo 'hosts: files dns' >| /etc/nsswitch.conf
|
||||
|
||||
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
|
||||
org.label-schema.name="oCIS WebDAV" \
|
||||
org.label-schema.vendor="ownCloud GmbH" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
EXPOSE 9115 9119
|
||||
|
||||
ENTRYPOINT ["/usr/bin/ocis-webdav"]
|
||||
CMD ["server"]
|
||||
|
||||
COPY bin/ocis-webdav /usr/bin/ocis-webdav
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM arm32v6/alpine:latest
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add ca-certificates mailcap && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
echo 'hosts: files dns' >| /etc/nsswitch.conf
|
||||
|
||||
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
|
||||
org.label-schema.name="oCIS WebDAV" \
|
||||
org.label-schema.vendor="ownCloud GmbH" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
EXPOSE 9115 9119
|
||||
|
||||
ENTRYPOINT ["/usr/bin/ocis-webdav"]
|
||||
CMD ["server"]
|
||||
|
||||
COPY bin/ocis-webdav /usr/bin/ocis-webdav
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM arm64v8/alpine:latest
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add ca-certificates mailcap && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
echo 'hosts: files dns' >| /etc/nsswitch.conf
|
||||
|
||||
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
|
||||
org.label-schema.name="oCIS WebDAV" \
|
||||
org.label-schema.vendor="ownCloud GmbH" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
EXPOSE 9115 9119
|
||||
|
||||
ENTRYPOINT ["/usr/bin/ocis-webdav"]
|
||||
CMD ["server"]
|
||||
|
||||
COPY bin/ocis-webdav /usr/bin/ocis-webdav
|
||||
@@ -0,0 +1,22 @@
|
||||
image: owncloud/ocis-webdav:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
- image: owncloud/ocis-webdav:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: owncloud/ocis-webdav:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
variant: v8
|
||||
os: linux
|
||||
- image: owncloud/ocis-webdav:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
variant: v6
|
||||
os: linux
|
||||
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "health",
|
||||
Usage: "check health status",
|
||||
Category: "info",
|
||||
Before: func(c *cli.Context) error {
|
||||
return parser.ParseConfig(cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
|
||||
resp, err := http.Get(
|
||||
fmt.Sprintf(
|
||||
"http://%s/healthz",
|
||||
cfg.Debug.Addr,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to request health check")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Fatal().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health seems to be in bad state")
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health got a good state")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/clihelper"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) cli.Commands {
|
||||
return []*cli.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the ocis-webdav command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cli.App{
|
||||
Name: "webdav",
|
||||
Usage: "Serve WebDAV API for oCIS",
|
||||
Commands: GetCommands(cfg),
|
||||
})
|
||||
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "help,h",
|
||||
Usage: "Show the help",
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
// SutureService allows for the webdav command to be embedded and supervised by a suture supervisor tree.
|
||||
type SutureService struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSutureService creates a new webdav.SutureService
|
||||
func NewSutureService(cfg *ociscfg.Config) suture.Service {
|
||||
cfg.Proxy.Commons = cfg.Commons
|
||||
return SutureService{
|
||||
cfg: cfg.WebDAV,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SutureService) Serve(ctx context.Context) error {
|
||||
s.cfg.Context = ctx
|
||||
if err := Execute(s.cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/logging"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/metrics"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/server/debug"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/server/http"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/tracing"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "server",
|
||||
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
Category: "server",
|
||||
Before: func(c *cli.Context) error {
|
||||
return parser.ParseConfig(cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
err := tracing.Configure(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
gr = run.Group{}
|
||||
ctx, cancel = func() (context.Context, context.CancelFunc) {
|
||||
if cfg.Context == nil {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
return context.WithCancel(cfg.Context)
|
||||
}()
|
||||
metrics = metrics.New()
|
||||
)
|
||||
|
||||
defer cancel()
|
||||
|
||||
metrics.BuildInfo.WithLabelValues(version.String).Set(1)
|
||||
|
||||
{
|
||||
server, err := http.Server(
|
||||
http.Logger(logger),
|
||||
http.Context(ctx),
|
||||
http.Config(cfg),
|
||||
http.Metrics(metrics),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Str("transport", "http").
|
||||
Msg("Failed to initialize server")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(func() error {
|
||||
return server.Run()
|
||||
}, func(err error) {
|
||||
logger.Error().Err(err).Msg("error ")
|
||||
logger.Info().
|
||||
Str("transport", "http").
|
||||
Msg("Shutting down server")
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
server, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(server.ListenAndServe, func(err error) {
|
||||
logger.Error().Err(err)
|
||||
_ = server.Shutdown(ctx)
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "print the version of this binary and the running extension instances",
|
||||
Category: "info",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Version: " + version.String)
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
fmt.Println("")
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No running " + cfg.Service.Name + " service found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
*shared.Commons `yaml:"-"`
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
Tracing *Tracing `yaml:"tracing"`
|
||||
Log *Log `yaml:"log"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
|
||||
OcisPublicURL string `yaml:"ocis_public_url" env:"OCIS_URL;OCIS_PUBLIC_URL"`
|
||||
WebdavNamespace string `yaml:"webdav_namespace" env:"STORAGE_WEBDAV_NAMESPACE"` //TODO: prevent this cross config
|
||||
RevaGateway string `yaml:"reva_gateway" env:"REVA_GATEWAY"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"WEBDAV_DEBUG_ADDR"`
|
||||
Token string `yaml:"token" env:"WEBDAV_DEBUG_TOKEN"`
|
||||
Pprof bool `yaml:"pprof" env:"WEBDAV_DEBUG_PPROF"`
|
||||
Zpages bool `yaml:"zpages" env:"WEBDAV_DEBUG_ZPAGES"`
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
)
|
||||
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:9119",
|
||||
Token: "",
|
||||
Pprof: false,
|
||||
Zpages: false,
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:9115",
|
||||
Root: "/",
|
||||
Namespace: "com.owncloud.web",
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "webdav",
|
||||
},
|
||||
OcisPublicURL: "https://127.0.0.1:9200",
|
||||
WebdavNamespace: "/users/{{.Id.OpaqueId}}",
|
||||
RevaGateway: "127.0.0.1:9142",
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
|
||||
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
|
||||
cfg.Log = &config.Log{
|
||||
Level: cfg.Commons.Log.Level,
|
||||
Pretty: cfg.Commons.Log.Pretty,
|
||||
Color: cfg.Commons.Log.Color,
|
||||
File: cfg.Commons.Log.File,
|
||||
}
|
||||
} else if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
Enabled: cfg.Commons.Tracing.Enabled,
|
||||
Type: cfg.Commons.Tracing.Type,
|
||||
Endpoint: cfg.Commons.Tracing.Endpoint,
|
||||
Collector: cfg.Commons.Tracing.Collector,
|
||||
}
|
||||
} else if cfg.Tracing == nil {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
}
|
||||
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
if cfg.HTTP.Root != "/" {
|
||||
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
// CORS defines the available cors configuration.
|
||||
type CORS struct {
|
||||
AllowedOrigins []string `yaml:"allowed_origins"`
|
||||
AllowedMethods []string `yaml:"allowed_methods"`
|
||||
AllowedHeaders []string `yaml:"allowed_headers"`
|
||||
AllowCredentials bool `yaml:"allow_credentials"`
|
||||
}
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"WEBDAV_HTTP_ADDR"`
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"WEBDAV_HTTP_ROOT"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;WEBDAV_LOG_LEVEL"`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;WEBDAV_LOG_PRETTY"`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;WEBDAV_LOG_COLOR"`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;WEBDAV_LOG_FILE"`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config/defaults"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads accounts configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Tracing defines the available tracing configuration.
|
||||
type Tracing struct {
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;WEBDAV_TRACING_ENABLED"`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;WEBDAV_TRACING_TYPE"`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;WEBDAV_TRACING_ENDPOINT"`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;WEBDAV_TRACING_COLLECTOR"`
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultWidth defines the default width of a thumbnail
|
||||
DefaultWidth = 32
|
||||
// DefaultHeight defines the default height of a thumbnail
|
||||
DefaultHeight = 32
|
||||
)
|
||||
|
||||
// ThumbnailRequest combines all parameters provided when requesting a thumbnail
|
||||
type ThumbnailRequest struct {
|
||||
// The file path of the source file
|
||||
Filepath string
|
||||
// The file name of the source file including the extension
|
||||
Filename string
|
||||
// The file extension
|
||||
Extension string
|
||||
// The requested width of the thumbnail
|
||||
Width int32
|
||||
// The requested height of the thumbnail
|
||||
Height int32
|
||||
// In case of a public share the public link token.
|
||||
PublicLinkToken string
|
||||
// The Identifier from the requested URL
|
||||
Identifier string
|
||||
}
|
||||
|
||||
// ParseThumbnailRequest extracts all required parameters from a http request.
|
||||
func ParseThumbnailRequest(r *http.Request) (*ThumbnailRequest, error) {
|
||||
fp, id, err := extractFilePath(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := r.URL.Query()
|
||||
|
||||
width, height, err := parseDimensions(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ThumbnailRequest{
|
||||
Filepath: fp,
|
||||
Filename: filepath.Base(fp),
|
||||
Extension: filepath.Ext(fp),
|
||||
Width: int32(width),
|
||||
Height: int32(height),
|
||||
PublicLinkToken: chi.URLParam(r, "token"),
|
||||
Identifier: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// the url looks as followed
|
||||
//
|
||||
// /remote.php/dav/files/<user>/<filepath>
|
||||
//
|
||||
// User and filepath are dynamic and filepath can contain slashes
|
||||
// So using the URLParam function is not possible.
|
||||
func extractFilePath(r *http.Request) (string, string, error) {
|
||||
id := chi.URLParam(r, "id")
|
||||
id, err := url.QueryUnescape(id)
|
||||
if err != nil {
|
||||
return "", "", errors.New("could not unescape user")
|
||||
}
|
||||
if id != "" {
|
||||
parts := strings.SplitN(r.URL.Path, id, 2)
|
||||
return parts[1], id, nil
|
||||
}
|
||||
|
||||
// This is for public links
|
||||
token := chi.URLParam(r, "token")
|
||||
if token != "" {
|
||||
parts := strings.SplitN(r.URL.Path, token, 2)
|
||||
return parts[1], "", nil
|
||||
}
|
||||
return "", "", errors.New("could not extract file path")
|
||||
}
|
||||
|
||||
func parseDimensions(q url.Values) (int64, int64, error) {
|
||||
width, err := parseDimension(q.Get("x"), "width", DefaultWidth)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
height, err := parseDimension(q.Get("y"), "height", DefaultHeight)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return width, height, nil
|
||||
}
|
||||
|
||||
func parseDimension(d, name string, defaultValue int64) (int64, error) {
|
||||
if d == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
result, err := strconv.ParseInt(d, 10, 32)
|
||||
if err != nil || result < 1 {
|
||||
// The error message doesn't fit but for OC10 API compatibility reasons we have to set this.
|
||||
return 0, fmt.Errorf("Cannot set %s of 0 or smaller!", name)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// LoggerFromConfig initializes a service-specific logger instance.
|
||||
func Configure(name string, cfg *config.Log) log.Logger {
|
||||
return log.NewLogger(
|
||||
log.Name(name),
|
||||
log.Level(cfg.Level),
|
||||
log.Pretty(cfg.Pretty),
|
||||
log.Color(cfg.Color),
|
||||
log.File(cfg.File),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Namespace defines the namespace for the defines metrics.
|
||||
Namespace = "ocis"
|
||||
|
||||
// Subsystem defines the subsystem for the defines metrics.
|
||||
Subsystem = "webdav"
|
||||
)
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
// Counter *prometheus.CounterVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New() *Metrics {
|
||||
m := &Metrics{
|
||||
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
// Namespace: Namespace,
|
||||
// Subsystem: Subsystem,
|
||||
// Name: "greet_total",
|
||||
// Help: "How many greeting requests processed",
|
||||
// }, []string{}),
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "build_info",
|
||||
Help: "Build Information",
|
||||
}, []string{"version"}),
|
||||
}
|
||||
|
||||
// prometheus.Register(
|
||||
// m.Counter,
|
||||
// )
|
||||
|
||||
_ = prometheus.Register(
|
||||
m.BuildInfo,
|
||||
)
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/debug"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
)
|
||||
|
||||
// Server initializes the debug service and server.
|
||||
func Server(opts ...Option) (*http.Server, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return debug.NewService(
|
||||
debug.Logger(options.Logger),
|
||||
debug.Name(options.Config.Service.Name),
|
||||
debug.Version(version.String),
|
||||
debug.Address(options.Config.Debug.Addr),
|
||||
debug.Token(options.Config.Debug.Token),
|
||||
debug.Pprof(options.Config.Debug.Pprof),
|
||||
debug.Zpages(options.Config.Debug.Zpages),
|
||||
debug.Health(health(options.Config)),
|
||||
debug.Ready(ready(options.Config)),
|
||||
debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
), nil
|
||||
}
|
||||
|
||||
// health implements the health check.
|
||||
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ready implements the ready check.
|
||||
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/metrics"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Namespace string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []cli.Flag
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = val
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(val []cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, val...)
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the namespace option.
|
||||
func Namespace(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
svc "github.com/owncloud/ocis/extensions/webdav/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/cors"
|
||||
"github.com/owncloud/ocis/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
service := http.NewService(
|
||||
http.Logger(options.Logger),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Name(options.Config.Service.Name),
|
||||
http.Version(version.String),
|
||||
http.Address(options.Config.HTTP.Addr),
|
||||
http.Context(options.Context),
|
||||
http.Flags(options.Flags...),
|
||||
)
|
||||
|
||||
handle, err := svc.NewService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(
|
||||
chimiddleware.RealIP,
|
||||
chimiddleware.RequestID,
|
||||
middleware.NoCache,
|
||||
middleware.Cors(
|
||||
cors.Logger(options.Logger),
|
||||
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
),
|
||||
middleware.Secure,
|
||||
middleware.Version(
|
||||
options.Config.Service.Name,
|
||||
version.String,
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
{
|
||||
handle = svc.NewInstrument(handle, options.Metrics)
|
||||
handle = svc.NewLogging(handle, options.Logger)
|
||||
handle = svc.NewTracing(handle)
|
||||
}
|
||||
|
||||
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/metrics"
|
||||
)
|
||||
|
||||
// NewInstrument returns a service that instruments metrics.
|
||||
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
|
||||
return instrument{
|
||||
next: next,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
type instrument struct {
|
||||
next Service
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Thumbnail implements the Service interface.
|
||||
func (i instrument) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.Thumbnail(w, r)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// NewLogging returns a service that logs messages.
|
||||
func NewLogging(next Service, logger log.Logger) Service {
|
||||
return logging{
|
||||
next: next,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type logging struct {
|
||||
next Service
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Dummy implements the Service interface.
|
||||
func (l logging) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.Thumbnail(w, r)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware provides a function to set the middleware option.
|
||||
func Middleware(val ...func(http.Handler) http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Middleware = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/storage/utils/templates"
|
||||
"github.com/go-chi/render"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/dav/requests"
|
||||
thumbnailsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/thumbnails/v0"
|
||||
thumbnailssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/thumbnails/v0"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenHeader = "X-Access-Token"
|
||||
)
|
||||
|
||||
var (
|
||||
codesEnum = map[int]string{
|
||||
http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest",
|
||||
http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated",
|
||||
http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound",
|
||||
http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed",
|
||||
}
|
||||
)
|
||||
|
||||
// Service defines the extension handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
Thumbnail(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// NewService returns a service implementation for Service.
|
||||
func NewService(opts ...Option) (Service, error) {
|
||||
options := newOptions(opts...)
|
||||
conf := options.Config
|
||||
|
||||
m := chi.NewMux()
|
||||
m.Use(options.Middleware...)
|
||||
|
||||
gwc, err := pool.GetGatewayServiceClient(conf.RevaGateway)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := Webdav{
|
||||
config: conf,
|
||||
log: options.Logger,
|
||||
mux: m,
|
||||
thumbnailsClient: thumbnailssvc.NewThumbnailService("com.owncloud.api.thumbnails", grpc.DefaultClient),
|
||||
revaClient: gwc,
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
r.Get("/remote.php/dav/spaces/{id}/*", svc.SpacesThumbnail)
|
||||
r.Get("/remote.php/dav/files/{id}/*", svc.Thumbnail)
|
||||
r.Get("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnail)
|
||||
r.Head("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnailHead)
|
||||
})
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// Webdav defines implements the business logic for Service.
|
||||
type Webdav struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
mux *chi.Mux
|
||||
thumbnailsClient thumbnailssvc.ThumbnailService
|
||||
revaClient gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (g Webdav) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
g.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// SpacesThumbnail is the endpoint for retrieving thumbnails inside of spaces.
|
||||
func (g Webdav) SpacesThumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
t := r.Header.Get(TokenHeader)
|
||||
|
||||
fullPath := tr.Identifier + "!" + tr.Filepath
|
||||
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_Cs3Source{
|
||||
Cs3Source: &thumbnailsmsg.CS3Source{
|
||||
Path: fullPath,
|
||||
Authorization: t,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendThumbnailResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
// Thumbnail implements the Service interface.
|
||||
func (g Webdav) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
t := r.Header.Get(TokenHeader)
|
||||
ctx := metadata.AppendToOutgoingContext(r.Context(), TokenHeader, t)
|
||||
userRes, err := g.revaClient.GetUserByClaim(ctx, &userv1beta1.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: tr.Identifier,
|
||||
})
|
||||
if err != nil || userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
g.log.Error().Err(err).Msg("could not get user")
|
||||
renderError(w, r, errInternalError("could not get user"))
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(templates.WithUser(userRes.User, g.config.WebdavNamespace), tr.Filepath)
|
||||
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_Cs3Source{
|
||||
Cs3Source: &thumbnailsmsg.CS3Source{
|
||||
Path: fullPath,
|
||||
Authorization: t,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendThumbnailResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_WebdavSource{
|
||||
WebdavSource: &thumbnailsmsg.WebdavSource{
|
||||
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
|
||||
IsPublicLink: true,
|
||||
PublicLinkToken: tr.PublicLinkToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
g.sendThumbnailResponse(rsp, w, r)
|
||||
}
|
||||
|
||||
func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
|
||||
tr, err := requests.ParseThumbnailRequest(r)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("could not create Request")
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
|
||||
Filepath: strings.TrimLeft(tr.Filepath, "/"),
|
||||
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
|
||||
Width: tr.Width,
|
||||
Height: tr.Height,
|
||||
Source: &thumbnailssvc.GetThumbnailRequest_WebdavSource{
|
||||
WebdavSource: &thumbnailsmsg.WebdavSource{
|
||||
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
|
||||
IsPublicLink: true,
|
||||
PublicLinkToken: tr.PublicLinkToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
e := merrors.Parse(err.Error())
|
||||
switch e.Code {
|
||||
case http.StatusNotFound:
|
||||
// StatusNotFound is expected for unsupported files
|
||||
renderError(w, r, errNotFound(notFoundMsg(tr.Filename)))
|
||||
return
|
||||
case http.StatusBadRequest:
|
||||
renderError(w, r, errBadRequest(err.Error()))
|
||||
default:
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
}
|
||||
g.log.Error().Err(err).Msg("could not get thumbnail")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (g Webdav) sendThumbnailResponse(rsp *thumbnailssvc.GetThumbnailResponse, w http.ResponseWriter, r *http.Request) {
|
||||
client := &http.Client{
|
||||
// Timeout: time.Second * 5,
|
||||
}
|
||||
|
||||
dlReq, err := http.NewRequest(http.MethodGet, rsp.DataEndpoint, http.NoBody)
|
||||
if err != nil {
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
g.log.Error().Err(err).Msg("could not download thumbnail")
|
||||
return
|
||||
}
|
||||
dlReq.Header.Set("Transfer-Token", rsp.TransferToken)
|
||||
|
||||
dlRsp, err := client.Do(dlReq)
|
||||
if err != nil {
|
||||
renderError(w, r, errInternalError(err.Error()))
|
||||
g.log.Error().Err(err).Msg("could not download thumbnail")
|
||||
return
|
||||
}
|
||||
defer dlRsp.Body.Close()
|
||||
|
||||
if dlRsp.StatusCode != http.StatusOK {
|
||||
g.log.Error().
|
||||
Str("transfer_token", rsp.TransferToken).
|
||||
Str("data_endpoint", rsp.DataEndpoint).
|
||||
Str("response_status", dlRsp.Status).
|
||||
Msg("could not download thumbnail")
|
||||
renderError(w, r, errInternalError("could not download thumbnail"))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", rsp.Mimetype)
|
||||
_, err = io.Copy(w, dlRsp.Body)
|
||||
if err != nil {
|
||||
g.log.Error().Err(err).Msg("failed to write thumbnail to response writer")
|
||||
}
|
||||
}
|
||||
|
||||
func extensionToThumbnailType(ext string) thumbnailsmsg.ThumbnailType {
|
||||
switch strings.ToUpper(ext) {
|
||||
case "GIF":
|
||||
return thumbnailsmsg.ThumbnailType_GIF
|
||||
case "PNG":
|
||||
return thumbnailsmsg.ThumbnailType_PNG
|
||||
default:
|
||||
return thumbnailsmsg.ThumbnailType_JPG
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
|
||||
type errResponse struct {
|
||||
HTTPStatusCode int `json:"-" xml:"-"`
|
||||
XMLName xml.Name `xml:"d:error"`
|
||||
Xmlnsd string `xml:"xmlns:d,attr"`
|
||||
Xmlnss string `xml:"xmlns:s,attr"`
|
||||
Exception string `xml:"s:exception"`
|
||||
Message string `xml:"s:message"`
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func newErrResponse(statusCode int, msg string) *errResponse {
|
||||
rsp := &errResponse{
|
||||
HTTPStatusCode: statusCode,
|
||||
Xmlnsd: "DAV",
|
||||
Xmlnss: "http://sabredav.org/ns",
|
||||
Exception: codesEnum[statusCode],
|
||||
}
|
||||
if msg != "" {
|
||||
rsp.Message = msg
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
func errInternalError(msg string) *errResponse {
|
||||
return newErrResponse(http.StatusInternalServerError, msg)
|
||||
}
|
||||
|
||||
func errBadRequest(msg string) *errResponse {
|
||||
return newErrResponse(http.StatusBadRequest, msg)
|
||||
}
|
||||
|
||||
func errNotFound(msg string) *errResponse {
|
||||
return newErrResponse(http.StatusNotFound, msg)
|
||||
}
|
||||
|
||||
func renderError(w http.ResponseWriter, r *http.Request, err *errResponse) {
|
||||
render.Status(r, err.HTTPStatusCode)
|
||||
render.XML(w, r, err)
|
||||
}
|
||||
|
||||
func notFoundMsg(name string) string {
|
||||
return "File with name " + name + " could not be located"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewTracing returns a service that instruments traces.
|
||||
func NewTracing(next Service) Service {
|
||||
return tracing{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
type tracing struct {
|
||||
next Service
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Thumbnail implements the Service interface.
|
||||
func (t tracing) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.Thumbnail(w, r)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/extensions/webdav/pkg/config"
|
||||
pkgtrace "github.com/owncloud/ocis/ocis-pkg/tracing"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// TraceProvider is the global trace provider for the proxy service.
|
||||
TraceProvider = trace.NewNoopTracerProvider()
|
||||
)
|
||||
|
||||
func Configure(cfg *config.Config) error {
|
||||
var err error
|
||||
if cfg.Tracing.Enabled {
|
||||
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# backend
|
||||
-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-webdav-debug && bin/ocis-webdav-debug --log-level debug server --debug-pprof --debug-zpages'
|
||||
Reference in New Issue
Block a user