init
This commit is contained in:
commit
bf3d3db898
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.idea
|
13
LICENSE
Normal file
13
LICENSE
Normal 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
4
README.md
Normal 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
36
dir.go
Normal 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
74
file.go
Normal 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
12
go.mod
Normal 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
18
go.sum
Normal 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
277
vfs.go
Normal 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
43
vfs_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user