implement first prototype of the logo upload API
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
package assetsfs
|
package assetsfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
"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
|
// 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) {
|
func (f *FileSystem) Open(original string) (http.File, error) {
|
||||||
if f.assetPath != "" {
|
if f.assetPath != "" {
|
||||||
file, err := read(f.assetPath, original)
|
file, err := os.Open(filepath.Join(f.assetPath, original))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return file, 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)
|
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 `<assetPath>/<name>`. It makes sure that the path is
|
||||||
|
// always under `<assetPath>` 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
|
// 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")
|
f, err := fs.Sub(embedFS, "assets")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Cannot load subtree fs:", err.Error())
|
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,
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,13 @@ package assets
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"golang.org/x/net/html"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fileServer struct {
|
type fileServer struct {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package defaults
|
package defaults
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
|
||||||
"github.com/owncloud/ocis/v2/services/web/pkg/config"
|
"github.com/owncloud/ocis/v2/services/web/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ func DefaultConfig() *config.Config {
|
|||||||
Name: "web",
|
Name: "web",
|
||||||
},
|
},
|
||||||
Asset: config.Asset{
|
Asset: config.Asset{
|
||||||
Path: "",
|
Path: filepath.Join(defaults.BaseDataPath(), "web/assets"),
|
||||||
},
|
},
|
||||||
Web: config.Web{
|
Web: config.Web{
|
||||||
Path: "",
|
Path: "",
|
||||||
|
|||||||
@@ -28,3 +28,8 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (i instrument) Config(w http.ResponseWriter, r *http.Request) {
|
func (i instrument) Config(w http.ResponseWriter, r *http.Request) {
|
||||||
i.next.Config(w, r)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,3 +28,8 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (l logging) Config(w http.ResponseWriter, r *http.Request) {
|
func (l logging) Config(w http.ResponseWriter, r *http.Request) {
|
||||||
l.next.Config(w, r)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ package svc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"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/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/assets"
|
||||||
"github.com/owncloud/ocis/v2/services/web/pkg/config"
|
"github.com/owncloud/ocis/v2/services/web/pkg/config"
|
||||||
)
|
)
|
||||||
@@ -25,6 +30,7 @@ var (
|
|||||||
type Service interface {
|
type Service interface {
|
||||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||||
Config(http.ResponseWriter, *http.Request)
|
Config(http.ResponseWriter, *http.Request)
|
||||||
|
UploadLogo(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a service implementation for Service.
|
// NewService returns a service implementation for Service.
|
||||||
@@ -38,10 +44,12 @@ func NewService(opts ...Option) Service {
|
|||||||
logger: options.Logger,
|
logger: options.Logger,
|
||||||
config: options.Config,
|
config: options.Config,
|
||||||
mux: m,
|
mux: m,
|
||||||
|
fs: assetsfs.New(web.Assets, options.Config.Asset.Path, options.Logger),
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||||
r.Get("/config.json", svc.Config)
|
r.Get("/config.json", svc.Config)
|
||||||
|
r.Post("/branding/logo", svc.UploadLogo)
|
||||||
r.Mount("/", svc.Static(options.Config.HTTP.CacheTTL))
|
r.Mount("/", svc.Static(options.Config.HTTP.CacheTTL))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,6 +66,7 @@ type Web struct {
|
|||||||
logger log.Logger
|
logger log.Logger
|
||||||
config *config.Config
|
config *config.Config
|
||||||
mux *chi.Mux
|
mux *chi.Mux
|
||||||
|
fs *assetsfs.FileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements the Service interface.
|
// ServeHTTP implements the Service interface.
|
||||||
@@ -131,12 +140,7 @@ func (p Web) Static(ttl int) http.HandlerFunc {
|
|||||||
|
|
||||||
static := http.StripPrefix(
|
static := http.StripPrefix(
|
||||||
rootWithSlash,
|
rootWithSlash,
|
||||||
assets.FileServer(
|
assets.FileServer(p.fs),
|
||||||
assets.New(
|
|
||||||
assets.Logger(p.logger),
|
|
||||||
assets.Config(p.config),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lastModified := time.Now().UTC().Format(http.TimeFormat)
|
lastModified := time.Now().UTC().Format(http.TimeFormat)
|
||||||
@@ -161,3 +165,31 @@ func (p Web) Static(ttl int) http.HandlerFunc {
|
|||||||
static.ServeHTTP(w, r)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,3 +24,8 @@ func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (t tracing) Config(w http.ResponseWriter, r *http.Request) {
|
func (t tracing) Config(w http.ResponseWriter, r *http.Request) {
|
||||||
t.next.Config(w, r)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user