From 20e4e56d2846d4a0ed93f397e9dfa417c599eaf0 Mon Sep 17 00:00:00 2001 From: David Christofas Date: Tue, 7 Feb 2023 17:05:36 +0100 Subject: [PATCH] implement first prototype of the logo upload API --- ocis-pkg/assetsfs/assetsfs.go | 42 +++++++--------- services/web/pkg/assets/option.go | 50 ------------------- services/web/pkg/assets/server.go | 3 +- .../web/pkg/config/defaults/defaultconfig.go | 4 +- services/web/pkg/service/v0/instrument.go | 5 ++ services/web/pkg/service/v0/logging.go | 5 ++ services/web/pkg/service/v0/service.go | 44 +++++++++++++--- services/web/pkg/service/v0/tracing.go | 5 ++ 8 files changed, 76 insertions(+), 82 deletions(-) delete mode 100644 services/web/pkg/assets/option.go diff --git a/ocis-pkg/assetsfs/assetsfs.go b/ocis-pkg/assetsfs/assetsfs.go index 1b6e9cbacd..311f904662 100644 --- a/ocis-pkg/assetsfs/assetsfs.go +++ b/ocis-pkg/assetsfs/assetsfs.go @@ -1,12 +1,11 @@ package assetsfs import ( - "embed" "fmt" "io/fs" "net/http" "os" - "path" + "path/filepath" "github.com/owncloud/ocis/v2/ocis-pkg/log" ) @@ -21,22 +20,31 @@ type FileSystem struct { // Open checks if assetPath is set and tries to load from there. Falls back to fs if that is not possible func (f *FileSystem) Open(original string) (http.File, error) { if f.assetPath != "" { - file, err := read(f.assetPath, original) + file, err := os.Open(filepath.Join(f.assetPath, original)) if err == nil { return file, nil } - f.log.Warn(). - Str("path", f.assetPath). - Str("filename", original). - Str("error", err.Error()). - Msg("error reading from assetPath") } - return f.fs.Open(original) } +// Create creates a new file in the assetPath +func (f *FileSystem) Create(name string) (*os.File, error) { + fullPath := f.jailPath(name) + if err := os.MkdirAll(filepath.Dir(fullPath), 0770); err != nil { + return nil, err + } + return os.Create(fullPath) +} + +// jailPath returns the fullPath `/`. It makes sure that the path is +// always under `` to prevent directory traversal. +func (f *FileSystem) jailPath(name string) string { + return filepath.Join(f.assetPath, filepath.Join("/", name)) +} + // New initializes a new FileSystem. Quits on error -func New(embedFS embed.FS, assetPath string, logger log.Logger) *FileSystem { +func New(embedFS fs.FS, assetPath string, logger log.Logger) *FileSystem { f, err := fs.Sub(embedFS, "assets") if err != nil { fmt.Println("Cannot load subtree fs:", err.Error()) @@ -49,17 +57,3 @@ func New(embedFS embed.FS, assetPath string, logger log.Logger) *FileSystem { log: logger, } } - -// tries to read file from disk or errors -func read(assetPath string, fileName string) (http.File, error) { - if stat, err := os.Stat(assetPath); err != nil || !stat.IsDir() { - return nil, fmt.Errorf("can't load asset path: %s", err) - } - - p := path.Join(assetPath, fileName) - if _, err := os.Stat(p); err != nil { - return nil, err - } - - return os.Open(p) -} diff --git a/services/web/pkg/assets/option.go b/services/web/pkg/assets/option.go deleted file mode 100644 index c839c29ddd..0000000000 --- a/services/web/pkg/assets/option.go +++ /dev/null @@ -1,50 +0,0 @@ -package assets - -import ( - "net/http" - - "github.com/owncloud/ocis/v2/ocis-pkg/assetsfs" - "github.com/owncloud/ocis/v2/ocis-pkg/log" - "github.com/owncloud/ocis/v2/services/web" - "github.com/owncloud/ocis/v2/services/web/pkg/config" -) - -// New returns a new http filesystem to serve assets. -func New(opts ...Option) http.FileSystem { - options := newOptions(opts...) - return assetsfs.New(web.Assets, options.Config.Asset.Path, options.Logger) -} - -// 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 -} - -// 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 - } -} diff --git a/services/web/pkg/assets/server.go b/services/web/pkg/assets/server.go index 7b139e8198..0e487cc29d 100644 --- a/services/web/pkg/assets/server.go +++ b/services/web/pkg/assets/server.go @@ -2,12 +2,13 @@ package assets import ( "bytes" - "golang.org/x/net/html" "io" "mime" "net/http" "path" "path/filepath" + + "golang.org/x/net/html" ) type fileServer struct { diff --git a/services/web/pkg/config/defaults/defaultconfig.go b/services/web/pkg/config/defaults/defaultconfig.go index f32c1f34dc..e4579166f8 100644 --- a/services/web/pkg/config/defaults/defaultconfig.go +++ b/services/web/pkg/config/defaults/defaultconfig.go @@ -1,8 +1,10 @@ package defaults import ( + "path/filepath" "strings" + "github.com/owncloud/ocis/v2/ocis-pkg/config/defaults" "github.com/owncloud/ocis/v2/services/web/pkg/config" ) @@ -31,7 +33,7 @@ func DefaultConfig() *config.Config { Name: "web", }, Asset: config.Asset{ - Path: "", + Path: filepath.Join(defaults.BaseDataPath(), "web/assets"), }, Web: config.Web{ Path: "", diff --git a/services/web/pkg/service/v0/instrument.go b/services/web/pkg/service/v0/instrument.go index 2d89f069d6..72c64082ce 100644 --- a/services/web/pkg/service/v0/instrument.go +++ b/services/web/pkg/service/v0/instrument.go @@ -28,3 +28,8 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (i instrument) Config(w http.ResponseWriter, r *http.Request) { i.next.Config(w, r) } + +// UploadLogo implements the Service interface. +func (i instrument) UploadLogo(w http.ResponseWriter, r *http.Request) { + i.next.UploadLogo(w, r) +} diff --git a/services/web/pkg/service/v0/logging.go b/services/web/pkg/service/v0/logging.go index 9e2c9e31ea..09ab4145e0 100644 --- a/services/web/pkg/service/v0/logging.go +++ b/services/web/pkg/service/v0/logging.go @@ -28,3 +28,8 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (l logging) Config(w http.ResponseWriter, r *http.Request) { l.next.Config(w, r) } + +// UploadLogo implements the Service interface. +func (l logging) UploadLogo(w http.ResponseWriter, r *http.Request) { + l.next.UploadLogo(w, r) +} diff --git a/services/web/pkg/service/v0/service.go b/services/web/pkg/service/v0/service.go index aeb1c3083d..dc4cfce938 100644 --- a/services/web/pkg/service/v0/service.go +++ b/services/web/pkg/service/v0/service.go @@ -2,16 +2,21 @@ package svc import ( "encoding/json" + "errors" "fmt" + "io" "net/http" "net/url" "os" + "path/filepath" "strconv" "strings" "time" "github.com/go-chi/chi/v5" + "github.com/owncloud/ocis/v2/ocis-pkg/assetsfs" "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/web" "github.com/owncloud/ocis/v2/services/web/pkg/assets" "github.com/owncloud/ocis/v2/services/web/pkg/config" ) @@ -25,6 +30,7 @@ var ( type Service interface { ServeHTTP(http.ResponseWriter, *http.Request) Config(http.ResponseWriter, *http.Request) + UploadLogo(http.ResponseWriter, *http.Request) } // NewService returns a service implementation for Service. @@ -38,10 +44,12 @@ func NewService(opts ...Option) Service { logger: options.Logger, config: options.Config, mux: m, + fs: assetsfs.New(web.Assets, options.Config.Asset.Path, options.Logger), } m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Get("/config.json", svc.Config) + r.Post("/branding/logo", svc.UploadLogo) r.Mount("/", svc.Static(options.Config.HTTP.CacheTTL)) }) @@ -58,6 +66,7 @@ type Web struct { logger log.Logger config *config.Config mux *chi.Mux + fs *assetsfs.FileSystem } // ServeHTTP implements the Service interface. @@ -131,12 +140,7 @@ func (p Web) Static(ttl int) http.HandlerFunc { static := http.StripPrefix( rootWithSlash, - assets.FileServer( - assets.New( - assets.Logger(p.logger), - assets.Config(p.config), - ), - ), + assets.FileServer(p.fs), ) lastModified := time.Now().UTC().Format(http.TimeFormat) @@ -161,3 +165,31 @@ func (p Web) Static(ttl int) http.HandlerFunc { static.ServeHTTP(w, r) } } + +// UploadLogo implements the endpoint to upload a custom logo for the oCIS instance. +func (p Web) UploadLogo(w http.ResponseWriter, r *http.Request) { + file, fileHeader, err := r.FormFile("logo") + if err != nil { + if errors.Is(err, http.ErrMissingFile) { + w.WriteHeader(http.StatusBadRequest) + } + w.WriteHeader(http.StatusInternalServerError) + return + } + defer file.Close() + + dst, err := p.fs.Create(filepath.Join("branding", filepath.Join("/", fileHeader.Filename))) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + defer dst.Close() + + _, err = io.Copy(dst, file) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/services/web/pkg/service/v0/tracing.go b/services/web/pkg/service/v0/tracing.go index 0b44df2bfb..2889715bdc 100644 --- a/services/web/pkg/service/v0/tracing.go +++ b/services/web/pkg/service/v0/tracing.go @@ -24,3 +24,8 @@ func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (t tracing) Config(w http.ResponseWriter, r *http.Request) { t.next.Config(w, r) } + +// UploadLogo implements the Service interface. +func (t tracing) UploadLogo(w http.ResponseWriter, r *http.Request) { + t.next.UploadLogo(w, r) +}