From 86109c0f09f15dd5a3303ad3476bcdfacd504b8a Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 16 Feb 2021 22:57:59 -0600 Subject: [PATCH] Initial Commit Signed-off-by: jolheiser --- .gitignore | 4 ++ LICENSE | 19 ++++++++++ Makefile | 17 +++++++++ README.md | 58 ++++++++++++++++++++++++++++ _test/disk/test2.txt | 1 + _test/embed/test1.txt | 1 + _test/embed/test2.txt | 1 + bench.txt | 9 +++++ go.mod | 3 ++ xtfs.go | 79 ++++++++++++++++++++++++++++++++++++++ xtfs_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 280 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 _test/disk/test2.txt create mode 100644 _test/embed/test1.txt create mode 100644 _test/embed/test2.txt create mode 100644 bench.txt create mode 100644 go.mod create mode 100644 xtfs.go create mode 100644 xtfs_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29ffcb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# GoLand +.idea/ + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..433f7db --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cb9577f --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +GO ?= go + +.PHONY: vet +vet: + $(GO) vet ./... + +.PHONY: fmt +fmt: + $(GO) fmt ./... + +.PHONY: test +test: + $(GO) test -race ./... + +.PHONY: bench +bench: + $(GO) test -benchmem -bench=. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4486d8 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# XTFS +eXTended File System + +XTFS is an easy way to implement a file system in such a way that +production assets can be overridden by assets on disk. + +## Usage + +```go +package main + +import ( + "embed" + + "go.jolheiser.com/xtfs" +) + +//go:embed assets +var assets embed.FS + +func main() { + xfs, err := xtfs.New("/var/lib/myapp/custom", assets) + if err != nil { + panic(err) + } + ... +} +``` + +If `/var/lib/myapp/custom` has an `assets` sub-directory, this implementation works. + +However, if `/var/lib/myapp/custom` matches the `assets` directory layout, +you can use `WithSub` like so... + +```go +package main + +import ( + "embed" + + "go.jolheiser.com/xtfs" +) + +//go:embed assets +var assets embed.FS + +func main() { + xfs, err := xtfs.New("/var/lib/myapp/custom", assets, xtfs.WithSub("assets")) + if err != nil { + panic(err) + } + ... +} +``` + +## License + +[MIT](LICENSE) \ No newline at end of file diff --git a/_test/disk/test2.txt b/_test/disk/test2.txt new file mode 100644 index 0000000..29f446a --- /dev/null +++ b/_test/disk/test2.txt @@ -0,0 +1 @@ +test3 \ No newline at end of file diff --git a/_test/embed/test1.txt b/_test/embed/test1.txt new file mode 100644 index 0000000..f079749 --- /dev/null +++ b/_test/embed/test1.txt @@ -0,0 +1 @@ +test1 \ No newline at end of file diff --git a/_test/embed/test2.txt b/_test/embed/test2.txt new file mode 100644 index 0000000..d606037 --- /dev/null +++ b/_test/embed/test2.txt @@ -0,0 +1 @@ +test2 \ No newline at end of file diff --git a/bench.txt b/bench.txt new file mode 100644 index 0000000..719cc74 --- /dev/null +++ b/bench.txt @@ -0,0 +1,9 @@ +go test -benchmem -bench=. +goos: linux +goarch: amd64 +pkg: go.jolheiser.com/xtfs +cpu: Intel(R) Core(TM) i7-4700MQ CPU @ 2.40GHz +BenchmarkCache-8 133917188 8.970 ns/op 0 B/op 0 allocs/op +BenchmarkNoCache-8 931821 1362 ns/op 280 B/op 4 allocs/op +PASS +ok go.jolheiser.com/xtfs 3.394s diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..158d9ab --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go.jolheiser.com/xtfs + +go 1.16 diff --git a/xtfs.go b/xtfs.go new file mode 100644 index 0000000..eaa03cd --- /dev/null +++ b/xtfs.go @@ -0,0 +1,79 @@ +package xtfs + +import ( + "io/fs" + "os" + "path" +) + +// XTFS is an eXTended File System +type XTFS struct { + fs fs.FS + root string + doCache bool + cache map[string]bool +} + +func (x *XTFS) apn(name string) string { + return path.Join(x.root, name) +} + +func (x *XTFS) exists(name string) bool { + if has, ok := x.cache[name]; ok && x.doCache { + return has + } + _, err := os.Stat(x.apn(name)) + if err != nil { + x.cache[name] = false + return false + } + x.cache[name] = true + return true +} + +// Open opens an fs.File, preferring disk +func (x *XTFS) Open(name string) (fs.File, error) { + if x.exists(name) { + return os.Open(x.apn(name)) + } + return x.fs.Open(name) +} + +// Option is a functional option for an XTFS +type Option func(*XTFS) error + +// New returns a new XTFS +func New(root string, fs fs.FS, opts ...Option) (*XTFS, error) { + x := &XTFS{ + fs: fs, + root: root, + doCache: true, + cache: make(map[string]bool), + } + + for _, opt := range opts { + if err := opt(x); err != nil { + return x, err + } + } + + return x, nil +} + +// WithSub sets a fs.Sub for an XTFS +func WithSub(sub string) Option { + return func(x *XTFS) (err error) { + x.fs, err = fs.Sub(x.fs, sub) + return + } +} + +// WithCaching sets a caching mode for an XTFS +// Caching avoids subsequent os.Stat to determine if a file exists on disk +// See bench.txt for differences in usage +func WithCaching(doCache bool) Option { + return func(x *XTFS) error { + x.doCache = doCache + return nil + } +} diff --git a/xtfs_test.go b/xtfs_test.go new file mode 100644 index 0000000..5db2b7e --- /dev/null +++ b/xtfs_test.go @@ -0,0 +1,88 @@ +package xtfs + +import ( + "embed" + "io" + "os" + "strings" + "testing" +) + +var ( + //go:embed _test/embed + embedded embed.FS + xtfs *XTFS +) + +func TestMain(m *testing.M) { + var err error + xtfs, err = New("_test/disk", embedded, WithSub("_test/embed"), WithCaching(false)) + if err != nil { + panic(err) + } + os.Exit(m.Run()) +} + +func TestEmbed(t *testing.T) { + fi, err := xtfs.Open("test1.txt") + if err != nil { + t.Log(err) + t.FailNow() + } + defer fi.Close() + + test1, err := io.ReadAll(fi) + if err != nil { + t.Log(err) + t.FailNow() + } + + if !strings.EqualFold(string(test1), "test1") { + t.Log("embed did not match") + t.FailNow() + } +} + +func TestDisk(t *testing.T) { + fi, err := xtfs.Open("test2.txt") + if err != nil { + t.Log(err) + t.FailNow() + } + defer fi.Close() + + test2, err := io.ReadAll(fi) + if err != nil { + t.Log(err) + t.FailNow() + } + + if !strings.EqualFold(string(test2), "test3") { + t.Log("embed did not match") + t.FailNow() + } +} + +func BenchmarkCache(b *testing.B) { + x, err := New("_test/disk", embedded) + if err != nil { + b.Log(err) + b.FailNow() + } + + for idx := 0; idx < b.N; idx++ { + x.exists("test2.txt") + } +} + +func BenchmarkNoCache(b *testing.B) { + x, err := New("_test/disk", embedded, WithCaching(false)) + if err != nil { + b.Log(err) + b.FailNow() + } + + for idx := 0; idx < b.N; idx++ { + x.exists("test2.txt") + } +}