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