[full-ci] enhancement: allow ocis to provide custom web applications (#8523)
* enhancement: allow ocis to provide custom web applications * enhancement: add an option to disable web apps * test: add default logger tests * test: add app loading tests * test: add asset server tests * enhancement: make use of dedicated app conf file and app asset paths * enhancement: adjust asset locations and deprecate WEB_ASSET_PATH * enhancement: get rid of default logger and use the service level logger instead * Apply suggestions from code review Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz> Co-authored-by: kobergj <juliankoberg@googlemail.com> * enhancement: use basename as app id * Apply suggestions from code review Co-authored-by: Martin <github@diemattels.at> * enhancement: use afero as fs abstraction * enhancement: simplify logo upload * enhancement: make use of introductionVersion field annotations --------- Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz> Co-authored-by: kobergj <juliankoberg@googlemail.com> Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
co-authored by
Benedikt Kulmann
kobergj
Martin
parent
6ba9e4adf7
commit
6814c61506
@@ -1,63 +0,0 @@
|
||||
package assetsfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// FileSystem customized to load assets
|
||||
type FileSystem struct {
|
||||
fs http.FileSystem
|
||||
assetPath string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// 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 := os.Open(filepath.Join(f.assetPath, original))
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
return f.fs.Open(original)
|
||||
}
|
||||
|
||||
func (f *FileSystem) OpenEmbedded(name string) (http.File, error) {
|
||||
return f.fs.Open(name)
|
||||
}
|
||||
|
||||
// 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
|
||||
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())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return &FileSystem{
|
||||
fs: http.FS(f),
|
||||
assetPath: assetPath,
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
gofig "github.com/gookit/config/v2"
|
||||
gooyaml "github.com/gookit/config/v2/yaml"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
|
||||
)
|
||||
|
||||
@@ -25,8 +26,7 @@ func BindSourcesToStructs(service string, dst interface{}) (*gofig.Config, error
|
||||
})
|
||||
cnf.AddDriver(gooyaml.Driver)
|
||||
|
||||
cfgFile := path.Join(defaults.BaseConfigPath(), service+".yaml")
|
||||
_ = cnf.LoadFiles([]string{cfgFile}...)
|
||||
_ = cnf.LoadFiles(path.Join(defaults.BaseConfigPath(), service+".yaml"))
|
||||
|
||||
err := cnf.BindStruct("", &dst)
|
||||
if err != nil {
|
||||
|
||||
+9
-1
@@ -10,10 +10,11 @@ import (
|
||||
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
mzlog "github.com/go-micro/plugins/v4/logger/zerolog"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go-micro.dev/v4/logger"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -148,3 +149,10 @@ func (l Logger) SubloggerWithRequestID(c context.Context) Logger {
|
||||
l.With().Str(RequestIDString, chimiddleware.GetReqID(c)).Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecation logs a deprecation message,
|
||||
// it is used to inform the user that a certain feature is deprecated and will be removed in the future.
|
||||
// Do not use a logger here because the message MUST be visible independent of the log level.
|
||||
func Deprecation(a ...any) {
|
||||
fmt.Printf("\033[1;31mDEPRECATION: %s\033[0m\n", a...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package log_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/owncloud/ocis/v2/internal/testenv"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
func TestDeprecation(t *testing.T) {
|
||||
cmdTest := testenv.NewCMDTest(t.Name())
|
||||
if cmdTest.ShouldRun() {
|
||||
log.Deprecation("this is a deprecation")
|
||||
return
|
||||
}
|
||||
|
||||
out, err := cmdTest.Run()
|
||||
|
||||
g := gomega.NewWithT(t)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
g.Expect(string(out)).To(gomega.HavePrefix("\033[1;31mDEPRECATION: this is a deprecation"))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package fsx
|
||||
|
||||
import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var (
|
||||
// assert interfaces implemented
|
||||
_ afero.Fs = (*FallbackFS)(nil)
|
||||
_ FS = (*FallbackFS)(nil)
|
||||
)
|
||||
|
||||
// FallbackFS is a filesystem that layers a primary filesystem on top of a secondary filesystem.
|
||||
type FallbackFS struct {
|
||||
FS
|
||||
primary *BaseFS
|
||||
secondary *BaseFS
|
||||
}
|
||||
|
||||
// Primary returns the primary filesystem.
|
||||
func (d *FallbackFS) Primary() *BaseFS {
|
||||
return d.primary
|
||||
}
|
||||
|
||||
// Secondary returns the secondary filesystem.
|
||||
func (d *FallbackFS) Secondary() *BaseFS {
|
||||
return d.secondary
|
||||
}
|
||||
|
||||
// NewFallbackFS returns a new FallbackFS instance.
|
||||
func NewFallbackFS(primary, secondary FS) *FallbackFS {
|
||||
return &FallbackFS{
|
||||
FS: FromAfero(afero.NewCopyOnWriteFs(secondary, primary)),
|
||||
primary: &BaseFS{Fs: primary},
|
||||
secondary: &BaseFS{Fs: secondary},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package fsx_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
|
||||
)
|
||||
|
||||
func TestLayeredFS(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
|
||||
read := func(fs fsx.FS, name string) (string, error) {
|
||||
f, err := fs.Open(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
mustRead := func(fs fsx.FS, name string) string {
|
||||
s, err := read(fs, name)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
create := func(fs fsx.FS, name, content string) {
|
||||
err := afero.WriteFile(fs, name, []byte(content), 0644)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
}
|
||||
|
||||
primary := fsx.NewMemMapFs()
|
||||
create(primary, "both.txt", "primary")
|
||||
g.Expect(mustRead(primary, "both.txt")).To(gomega.Equal("primary"))
|
||||
create(primary, "primary.txt", "primary")
|
||||
g.Expect(mustRead(primary, "primary.txt")).To(gomega.Equal("primary"))
|
||||
|
||||
secondary := fsx.NewMemMapFs()
|
||||
create(secondary, "both.txt", "secondary")
|
||||
g.Expect(mustRead(secondary, "both.txt")).To(gomega.Equal("secondary"))
|
||||
create(secondary, "secondary.txt", "secondary")
|
||||
g.Expect(mustRead(secondary, "secondary.txt")).To(gomega.Equal("secondary"))
|
||||
|
||||
fs := fsx.NewFallbackFS(primary, fsx.NewReadOnlyFs(secondary))
|
||||
g.Expect(mustRead(fs, "both.txt")).To(gomega.Equal("primary"))
|
||||
g.Expect(mustRead(fs, "primary.txt")).To(gomega.Equal("primary"))
|
||||
g.Expect(mustRead(fs, "secondary.txt")).To(gomega.Equal("secondary"))
|
||||
|
||||
create(fs, "fallback-fs.txt", "fallback-fs")
|
||||
g.Expect(mustRead(fs, "fallback-fs.txt")).To(gomega.Equal("fallback-fs"))
|
||||
g.Expect(mustRead(primary, "fallback-fs.txt")).To(gomega.Equal("fallback-fs"))
|
||||
g.Expect(mustRead(fs.Primary(), "fallback-fs.txt")).To(gomega.Equal("fallback-fs"))
|
||||
_, err := read(secondary, "fallback-fs.txt")
|
||||
g.Expect(err).To(gomega.HaveOccurred())
|
||||
_, err = read(fs.Secondary(), "fallback-fs.txt")
|
||||
g.Expect(err).To(gomega.HaveOccurred())
|
||||
}
|
||||
|
||||
func TestLayeredFS_Primary(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
primary := fsx.NewMemMapFs()
|
||||
fs := fsx.NewFallbackFS(primary, fsx.NewMemMapFs())
|
||||
|
||||
g.Expect(primary).To(gomega.BeIdenticalTo(fs.Primary().Fs))
|
||||
}
|
||||
|
||||
func TestLayeredFS_Secondary(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
secondary := fsx.NewMemMapFs()
|
||||
fs := fsx.NewFallbackFS(fsx.NewMemMapFs(), secondary)
|
||||
|
||||
g.Expect(secondary).To(gomega.BeIdenticalTo(fs.Secondary().Fs))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package fsx
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var (
|
||||
// assert interfaces implemented
|
||||
_ afero.Fs = (*BaseFS)(nil)
|
||||
_ FS = (*BaseFS)(nil)
|
||||
)
|
||||
|
||||
// FS is our default interface for filesystems.
|
||||
type FS interface {
|
||||
afero.Fs
|
||||
IOFS() fs.FS
|
||||
}
|
||||
|
||||
// BaseFS is our default implementation of the FS interface.
|
||||
type BaseFS struct {
|
||||
afero.Fs
|
||||
}
|
||||
|
||||
// IOFS returns the filesystem as an io/fs.FS.
|
||||
func (b *BaseFS) IOFS() fs.FS {
|
||||
return afero.NewIOFS(b)
|
||||
}
|
||||
|
||||
// FromAfero returns a new BaseFS instance from an afero.Fs.
|
||||
func FromAfero(fSys afero.Fs) *BaseFS {
|
||||
return &BaseFS{Fs: fSys}
|
||||
}
|
||||
|
||||
// FromIOFS returns a new BaseFS instance from an io/fs.FS.
|
||||
func FromIOFS(fSys fs.FS) *BaseFS {
|
||||
return FromAfero(&afero.FromIOFS{FS: fSys})
|
||||
}
|
||||
|
||||
// NewBasePathFs returns a new BaseFS which wraps the given filesystem with a base path.
|
||||
func NewBasePathFs(fSys FS, basePath string) *BaseFS {
|
||||
return FromAfero(afero.NewBasePathFs(fSys, basePath))
|
||||
}
|
||||
|
||||
// NewOsFs returns a new BaseFS which wraps the OS filesystem.
|
||||
func NewOsFs() *BaseFS {
|
||||
return FromAfero(afero.NewOsFs())
|
||||
}
|
||||
|
||||
// NewReadOnlyFs returns a new BaseFS which wraps the given filesystem with a read-only filesystem.
|
||||
func NewReadOnlyFs(FfSys FS) *BaseFS {
|
||||
return FromAfero(afero.NewReadOnlyFs(FfSys))
|
||||
}
|
||||
|
||||
// NewMemMapFs returns a new BaseFS which wraps the memory filesystem.
|
||||
func NewMemMapFs() *BaseFS {
|
||||
return FromAfero(afero.NewMemMapFs())
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package fsx_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
|
||||
)
|
||||
|
||||
func TestBase(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
wrapped := fsx.NewMemMapFs()
|
||||
fs := fsx.BaseFS{Fs: wrapped.Fs}
|
||||
|
||||
g.Expect(wrapped.Fs).Should(gomega.BeIdenticalTo(fs.Fs))
|
||||
}
|
||||
|
||||
func TestBase_IOFS(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
fs := fsx.BaseFS{Fs: fsx.NewMemMapFs()}
|
||||
g.Expect(reflect.TypeOf(fs.IOFS()).Name()).Should(gomega.Equal("IOFS"))
|
||||
}
|
||||
|
||||
func TestFromIOFS(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
fs := fsx.FromIOFS(os.DirFS("."))
|
||||
|
||||
g.Expect(reflect.TypeOf(fs.Fs).String()).Should(gomega.Equal("*afero.FromIOFS"))
|
||||
}
|
||||
|
||||
func TestNewOsFs(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
fs := fsx.NewOsFs()
|
||||
|
||||
g.Expect(reflect.TypeOf(fs.Fs).String()).Should(gomega.Equal("*afero.OsFs"))
|
||||
}
|
||||
|
||||
func TestNewBasePathFs(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
base := fsx.NewMemMapFs()
|
||||
|
||||
err := base.MkdirAll("first/foo/bar", 0755)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
err = base.MkdirAll("second/foo/baz", 0755)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
fs := fsx.NewBasePathFs(base, "first")
|
||||
|
||||
info, err := fs.Stat("/foo/bar")
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
g.Expect(info.IsDir()).To(gomega.BeTrue())
|
||||
|
||||
info, err = fs.Stat("/foo/baz")
|
||||
g.Expect(err).To(gomega.HaveOccurred())
|
||||
g.Expect(info).To(gomega.BeNil())
|
||||
|
||||
info, err = fs.Stat("../second/foo/baz")
|
||||
g.Expect(err).To(gomega.HaveOccurred())
|
||||
g.Expect(info).To(gomega.BeNil())
|
||||
}
|
||||
|
||||
func TestNewReadOnlyFs(t *testing.T) {
|
||||
g := gomega.NewWithT(t)
|
||||
base := fsx.NewMemMapFs()
|
||||
|
||||
err := base.MkdirAll("first/foo/bar", 0755)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
fs := fsx.NewReadOnlyFs(base)
|
||||
err = fs.MkdirAll("first/foo/bay", 0755)
|
||||
g.Expect(err).To(gomega.HaveOccurred())
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package filepathx
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// JailJoin joins any number of path elements into a single path,
|
||||
// it protects against directory traversal by removing any "../" elements
|
||||
// and ensuring that the path is always under the jail.
|
||||
func JailJoin(jail string, elem ...string) string {
|
||||
return filepath.Join(jail, filepath.Join(append([]string{"/"}, elem...)...))
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package filepathx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/path/filepathx"
|
||||
)
|
||||
|
||||
func TestJailJoin(t *testing.T) {
|
||||
type args struct {
|
||||
jail string
|
||||
elem []string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "regular use case",
|
||||
args: args{
|
||||
jail: "/",
|
||||
elem: []string{"a", "b", "c"},
|
||||
},
|
||||
want: "/a/b/c",
|
||||
},
|
||||
{
|
||||
name: "access parent directory",
|
||||
args: args{
|
||||
jail: "/",
|
||||
elem: []string{"a", "b", "c", ".."},
|
||||
},
|
||||
want: "/a/b",
|
||||
},
|
||||
{
|
||||
name: "restrict breaking out of jail",
|
||||
args: args{
|
||||
jail: "/",
|
||||
elem: []string{"a", "b", "c", "..", "..", "..", "..", "..", "..", ".."},
|
||||
},
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
name: "restrict to child of jail",
|
||||
args: args{
|
||||
jail: "/a/b",
|
||||
elem: []string{"a", "b", "c", "..", "..", "..", "..", "..", "..", ".."},
|
||||
},
|
||||
want: "/a/b",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := filepathx.JailJoin(tt.args.jail, tt.args.elem...); got != tt.want {
|
||||
t.Errorf("JailJoin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user