commit - /dev/null
commit + bf3d3db8987d619ab1b7e2d522ed79e316c98e72
blob - /dev/null
blob + 723ef36f4e4f32c4560383aa5987c575a30c6535 (mode 644)
--- /dev/null
+++ .gitignore
+.idea
\ No newline at end of file
blob - /dev/null
blob + 7ebe3eaf280520474938a7b3048d6ba18b8e6352 (mode 644)
--- /dev/null
+++ LICENSE
+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
+# 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
+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
+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
+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
+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
+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
+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)
+ }
+}