Commit Diff


commit - /dev/null
commit + bf3d3db8987d619ab1b7e2d522ed79e316c98e72
blob - /dev/null
blob + 723ef36f4e4f32c4560383aa5987c575a30c6535 (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1 @@
+.idea
\ No newline at end of file
blob - /dev/null
blob + 7ebe3eaf280520474938a7b3048d6ba18b8e6352 (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,13 @@
+Copyright 2023 Evan Burkey <dev@fputs.com>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + 92c945e6f04fb51936483414735d8b6f122c1318 (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,4 @@
+# vfs
+
+`vfs` is a virtual in-memory filesystem. Unlike other similar implementations, it is not designed to expand beyond
+a simple filesystem abstraction that lives in memory.
blob - /dev/null
blob + eac48c7629e0b0b5adca38ef6855c8f6823f7e53 (mode 644)
--- /dev/null
+++ dir.go
@@ -0,0 +1,36 @@
+package vfs
+
+import (
+	"fmt"
+	"io/fs"
+	"os"
+	"sync"
+)
+
+type directory struct {
+	name     string
+	mode     os.FileMode
+	children map[string]interface{}
+	mutex    sync.Mutex
+}
+
+type directoryHandle struct {
+	dir *directory
+	idx int
+}
+
+func (d *directoryHandle) Stat() (fs.FileInfo, error) {
+	return &fileInfo{
+		name: d.dir.name,
+		size: 4096,
+		mode: d.dir.mode | fs.ModeDir,
+	}, nil
+}
+
+func (d *directoryHandle) Read(data []byte) (int, error) {
+	return 0, fmt.Errorf("%s is a directory", d.dir.name)
+}
+
+func (d *directoryHandle) Close() error {
+	return nil
+}
blob - /dev/null
blob + 26591bc8c1cf54260c3caf7892d55860f67aa0b3 (mode 644)
--- /dev/null
+++ file.go
@@ -0,0 +1,74 @@
+package vfs
+
+import (
+	"bytes"
+	"io/fs"
+	"os"
+	"time"
+)
+
+type File struct {
+	name     string
+	mode     os.FileMode
+	modified time.Time
+	open     bool
+	data     *bytes.Buffer
+}
+
+type fileInfo struct {
+	name     string
+	size     int64
+	modified time.Time
+	mode     os.FileMode
+}
+
+func (f *File) Stat() (fs.FileInfo, error) {
+	if f.open {
+		return &fileInfo{
+			name:     f.name,
+			size:     int64(f.data.Len()),
+			modified: f.modified,
+			mode:     f.mode,
+		}, nil
+	}
+	return nil, fs.ErrClosed
+}
+
+func (f *File) Read(data []byte) (int, error) {
+	if f.open {
+		return f.data.Read(data)
+	}
+	return 0, fs.ErrClosed
+}
+
+func (f *File) Close() error {
+	if f.open {
+		f.open = false
+		return nil
+	}
+	return fs.ErrClosed
+}
+
+func (f fileInfo) Name() string {
+	return f.name
+}
+
+func (f fileInfo) Size() int64 {
+	return f.size
+}
+
+func (f fileInfo) Mode() fs.FileMode {
+	return f.mode
+}
+
+func (f fileInfo) ModTime() time.Time {
+	return f.modified
+}
+
+func (f fileInfo) IsDir() bool {
+	return f.mode == fs.ModeDir
+}
+
+func (f fileInfo) Sys() any {
+	return nil
+}
blob - /dev/null
blob + 9bce24c5fb21fc8e6f37ac11018f6b634426bab0 (mode 644)
--- /dev/null
+++ go.mod
@@ -0,0 +1,12 @@
+module vfs
+
+go 1.20
+
+require github.com/stretchr/testify v1.8.4
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/objx v0.5.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
blob - /dev/null
blob + 673e9186f2857385ae4348d89b3cfec9f84e7e44 (mode 644)
--- /dev/null
+++ go.sum
@@ -0,0 +1,18 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
blob - /dev/null
blob + 5c66b21f2af9716a8dde2f936f9bc6d162a9ec2e (mode 644)
--- /dev/null
+++ vfs.go
@@ -0,0 +1,277 @@
+package vfs
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/fs"
+	"os"
+	"path"
+	"strings"
+)
+
+type FS struct {
+	directory *directory
+}
+
+func NewVFS() *FS {
+	return &FS{
+		directory: &directory{
+			children: make(map[string]interface{}),
+		},
+	}
+}
+
+func (f *FS) Open(path string) (fs.File, error) {
+	if !fs.ValidPath(path) {
+		return nil, &fs.PathError{
+			Op:   "Open",
+			Path: path,
+			Err:  fs.ErrInvalid,
+		}
+	}
+
+	if path == "." {
+		path = ""
+	}
+
+	child, err := f.find(path)
+	if err != nil {
+		return nil, err
+	}
+
+	switch c := child.(type) {
+	case *File:
+		fp := &File{
+			name: c.name,
+			mode: c.mode,
+			data: bytes.NewBuffer(c.data.Bytes()),
+			open: true,
+		}
+		return fp, nil
+	case *directory:
+		return &directoryHandle{
+			dir: c,
+		}, nil
+	}
+
+	return nil, fmt.Errorf("%s is unknown file type %v", path, fs.ErrInvalid)
+}
+
+func (f *FS) find(path string) (interface{}, error) {
+	if path == "" {
+		return f.directory, nil
+	}
+
+	current := f.directory
+	split := strings.Split(path, "/")
+	var target interface{}
+
+	var err error
+	err = nil
+
+	for i, subpath := range split {
+		target, err = func() (interface{}, error) {
+			current.mutex.Lock()
+			defer current.mutex.Unlock()
+
+			child := current.children[subpath]
+			if child == nil {
+				return nil, fmt.Errorf("%s is not a directory", subpath)
+			}
+
+			if _, ok := child.(*File); ok {
+				if i == len(split)-1 {
+					return child, nil
+				}
+				return nil, fmt.Errorf("%s does not exist", path)
+			}
+
+			if subdir, ok := child.(*directory); !ok {
+				return nil, fmt.Errorf("directory %s does not exist", path)
+			} else {
+				current = subdir
+			}
+
+			return child, nil
+		}()
+	}
+
+	return target, err
+}
+
+func (f *FS) findDirectory(path string) (*directory, error) {
+	if path == "" {
+		return f.directory, nil
+	}
+
+	current := f.directory
+	split := strings.Split(path, "/")
+
+	for _, subpath := range split {
+		err := func() error {
+			current.mutex.Lock()
+			defer current.mutex.Unlock()
+
+			child := current.children[subpath]
+			if child == nil {
+				return fmt.Errorf("%s is not a directory", subpath)
+			}
+
+			if subdir, ok := child.(*directory); !ok {
+				return fmt.Errorf("directory %s does not exist", path)
+			} else {
+				current = subdir
+			}
+
+			return nil
+		}()
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return current, nil
+}
+
+func (f *FS) create(createPath string) (*File, error) {
+	if !fs.ValidPath(createPath) {
+		return nil, &fs.PathError{
+			Op:   "create",
+			Path: createPath,
+			Err:  fs.ErrInvalid,
+		}
+	}
+
+	if createPath == "." {
+		createPath = ""
+	}
+
+	dirName, fileName := path.Split(createPath)
+	dirName = strings.TrimSuffix(dirName, "/")
+
+	dir, err := f.findDirectory(dirName)
+	if err != nil {
+		return nil, err
+	}
+
+	dir.mutex.Lock()
+	defer dir.mutex.Unlock()
+
+	checkExist := dir.children[fileName]
+	if checkExist != nil {
+		if _, ok := checkExist.(*File); !ok {
+			return nil, fmt.Errorf("%s is a directory that already exists", createPath)
+		}
+	}
+
+	file := &File{
+		name: fileName,
+		mode: 0666,
+		data: &bytes.Buffer{},
+	}
+	dir.children[fileName] = file
+	return file, nil
+}
+
+func (f *FS) MkdirAll(path string, mode os.FileMode) error {
+	if !fs.ValidPath(path) {
+		return &fs.PathError{
+			Op:   "MkdirAll",
+			Path: path,
+			Err:  fs.ErrInvalid,
+		}
+	}
+
+	if path == "." {
+		return nil
+	}
+
+	split := strings.Split(path, "/")
+	next := f.directory
+
+	for _, subpath := range split {
+		current := next
+		current.mutex.Lock()
+
+		child := current.children[subpath]
+		if child == nil {
+			newDir := &directory{
+				name:     subpath,
+				mode:     mode,
+				children: make(map[string]interface{}),
+			}
+			current.children[subpath] = newDir
+			next = newDir
+		} else {
+			if childDir, ok := child.(*directory); !ok {
+				current.mutex.Unlock()
+				return fmt.Errorf("%s is not a directory", subpath)
+			} else {
+				next = childDir
+			}
+		}
+		current.mutex.Unlock()
+	}
+
+	return nil
+}
+
+func (f *FS) WriteFile(path string, data []byte, mode os.FileMode) error {
+	if !fs.ValidPath(path) {
+		return &fs.PathError{
+			Op:   "WriteFile",
+			Path: path,
+			Err:  fs.ErrInvalid,
+		}
+	}
+
+	if path == "." {
+		path = ""
+	}
+
+	file, err := f.create(path)
+	if err != nil {
+		return err
+	}
+	file.data = bytes.NewBuffer(data)
+	file.mode = mode
+
+	return nil
+}
+
+func (f *FS) LoadDirectory(fsPath, sourcePath string) error {
+	err := f.MkdirAll(fsPath, 0777)
+	if err != nil {
+		return err
+	}
+
+	files, err := os.ReadDir(sourcePath)
+	if err != nil {
+		return err
+	}
+
+	for _, file := range files {
+		if !file.IsDir() {
+			fp, err := os.Open(path.Join(sourcePath, file.Name()))
+			if err != nil {
+				return err
+			}
+			data, err := io.ReadAll(fp)
+			if err != nil {
+				return err
+			}
+			err = f.WriteFile(path.Join(fsPath, file.Name()), data, 0666)
+			if err != nil {
+				return err
+			}
+			err = fp.Close()
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
blob - /dev/null
blob + fcf03e0d395795eee9a02a0fc5ef1fc83b45a867 (mode 644)
--- /dev/null
+++ vfs_test.go
@@ -0,0 +1,43 @@
+package vfs
+
+import (
+	"github.com/stretchr/testify/assert"
+	"io"
+	"os"
+	"path"
+	"testing"
+)
+
+func TestVFS_Basic(t *testing.T) {
+	testcases := []struct {
+		name     string
+		dirPath  string
+		fileName string
+		content  []byte
+		mode     os.FileMode
+	}{
+		{"Simple Root", ".", "test.txt", []byte("some content"), 0666},
+		{"One Layer Path", "folder", "test.txt", []byte("some content"), 0666},
+		{"Multi-Layer Path", "folder/folder2/dang", "test.txt", []byte("some content"), 0666},
+	}
+	for _, testcase := range testcases {
+		vfs := NewVFS()
+		fullPath := path.Join(testcase.dirPath, testcase.fileName)
+
+		err := vfs.MkdirAll(testcase.dirPath, 0777)
+		assert.NoError(t, err)
+
+		err = vfs.WriteFile(fullPath, testcase.content, testcase.mode)
+		assert.NoError(t, err)
+
+		fp, err := vfs.Open(fullPath)
+		assert.NoError(t, err)
+
+		content, err := io.ReadAll(fp)
+		assert.NoError(t, err)
+		assert.Equal(t, content, testcase.content)
+
+		err = fp.Close()
+		assert.NoError(t, err)
+	}
+}