commit
86109c0f09
|
@ -0,0 +1,4 @@
|
||||||
|
# GoLand
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -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=.
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
test3
|
|
@ -0,0 +1 @@
|
||||||
|
test1
|
|
@ -0,0 +1 @@
|
||||||
|
test2
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue