This commit is contained in:
Evan Burkey 2023-07-27 15:33:43 -07:00
commit bf3d3db898
9 changed files with 478 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

13
LICENSE Normal file
View File

@ -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.

4
README.md Normal file
View File

@ -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.

36
dir.go Normal file
View File

@ -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
}

74
file.go Normal file
View File

@ -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
}

12
go.mod Normal file
View File

@ -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
)

18
go.sum Normal file
View File

@ -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=

277
vfs.go Normal file
View File

@ -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
}

43
vfs_test.go Normal file
View File

@ -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)
}
}