Initial QSfera import
This commit is contained in:
@@ -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/qsfera/server/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/qsfera/server/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,63 @@
|
||||
package filepathx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qsfera/server/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 {
|
||||
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