commit bf3d3db8987d619ab1b7e2d522ed79e316c98e72 Author: Evan Burkey Date: Thu Jul 27 15:33:43 2023 -0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ebe3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Evan Burkey + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..92c945e --- /dev/null +++ b/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. diff --git a/dir.go b/dir.go new file mode 100644 index 0000000..eac48c7 --- /dev/null +++ b/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 +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..26591bc --- /dev/null +++ b/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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9bce24c --- /dev/null +++ b/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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..673e918 --- /dev/null +++ b/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= diff --git a/vfs.go b/vfs.go new file mode 100644 index 0000000..5c66b21 --- /dev/null +++ b/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 +} diff --git a/vfs_test.go b/vfs_test.go new file mode 100644 index 0000000..fcf03e0 --- /dev/null +++ b/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) + } +}