fs

package module
v0.0.0-...-766272b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2025 License: MIT, Unlicense Imports: 13 Imported by: 0

README

fs

Hitting the hard drive during a test is still magnitudes slower than RAM. Having a golden directory/file-structure on which the tests of a package operate might raise concurrency issues if these tests should run in parallel. To test the (file system) error-behavior of production code we must create pathological use-cases which by nature may be rather difficult. The here provided FS-type and its surrounding functions/options addresses all of these use cases with their issues.

NOTE go talks 2012 and go fstest package give already good clues for how to do this. But using fstest.MapFS which implements "io/fs".FS has a few short comings. "io/fs".FS was designed for accessing files that were added to a go binary hence its implementation doesn't need to care about os-specifics, about permissions and being read only suffices. I.e. fstest.MapFS is not suitable to actually mock an operating system's file system in the sense that the mock behaves identical as the production os file system. E.g. os.Open("/home/me/a/test.file") is perfectly fine as long as the path exists while for an implementation of "io/fs".FS fs.FS.Open("/home/me/a/test.file") should fail ...

The here implemented FS-type can do both: emulating the behavior "io/fs".FS expects and behaving like os.Open and friends.

package main

import (
    "fmt"
    "log"

    "git.sr.ht/~slukits/fs"
)

func main() {
    fs := new(fs.FS)
    data, err := fs.ReadFile("/etc/bashrc")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
}

Calling go run . on above code prints

# System-wide .bashrc file for interactive bash(1) shells.
[...]

If we want to use fs.ReadFile in a relative manner we could retrieve the current working directory with os.Getwd() and calculate a relative path to /etc/bashrc. But the more typical behavior with "io/fs".FS is that it has one root from which all files of interest are accessible. I.e. we could use with the default implementation of fs.FS the os.Chdir function to change the current working directory to /etc.

package main

import (
    "fmt"
    "log"
    "os"

    "git.sr.ht/~slukits/fs"
)

func main() {
    fs := new(fs.FS)
    if err := os.Chdir("/etc"); err != nil {
        log.Fatal(err)
    }
    data, err := fs.ReadFile("bashrc")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
}

An other option which works for the default implementation and the in-memory implementation of fs.FS the same and avoids a potentially failing os.Chdir call is setting a file-system's working directory.

package main

import (
    "fmt"
    "log"

    "git.sr.ht/~slukits/fs"
)

func main() {
    fS := new(fs.FS)
    fS.Apply(fs.WD("/etc"))
    data, err := fS.ReadFile("bashrc")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
}

Because if the default implementation of FS gets an unrooted directory or file name (i.e. not prefixed by fs.SystemRoot) and has a working directory set the name is prefixed by set working directory.

Note the fs.FS type comes with the convenience constructor fs.CopyToMem creating from a given file-system a new in memory copy.

testing with fs.Fs

There are two main use cases for testing with fs.FS: mocking a production file system into memory, and mocking failing file system operations.

mocking failing file system operations

fs.FS allows us to adapt its methods which in turn allows for systematically testing of error cases

package main

import (
    "errors"
    "fmt"
    "testing"

    "git.sr.ht/~slukits/fs"
    "git.sr.ht/~slukits/exp"
)

var ErrProduction = errors.New("a production error")

func ProductionFunc(fs *fs.FS) error {
    bashrc, err := fs.ReadFile("/etc/bashrc")
    if err != nil {
        return fmt.Errorf("%w: %w", ErrProduction, err)
    }
    _ = bashrc
    return nil
}

func TestProductionFuncErrorBehavior(t *testing.T) {
    ErrMocked := errors.New("mocked read file error")
    fx := new(fs.FS)
    fx.Apply(fs.AdaptFS.ReadFile(
        func(_ fs.FnReadFile) fs.FnReadFile {
            return func(s string) ([]byte, error) {
                return nil, ErrMocked
            }
        },
    ))

    err := ProductionFunc(fx)

    exp := exp.ErrIs(t, err, ErrProduction)
    exp.ErrIs(err, ErrMocked)
}

The fs.AdaptFS.ReadFile-option asks for a callback function with the signature func(fs.FnReadFile) fs.FnReadFile. I.e. this function will get the currently used fs.FnReadFile to execute FS.ReadFile-calls and should return its replacement. As can be seen above for a typical use case we don't really need the currently used fs.FnReadFile function. But some times not each call of an adapted function should fail, or we want to reset an adapted function to its original behavior, or we want to tweak arguments or return values of a production call (see TestSnapshotToMemFailsIfSourceFileCantBeStated). This somewhat more elaborate adaption implementation has these use cases covered as well.

mocking production file system and its permissions

Assume the production code wants to modify /etc/bashrc. We will not want it to modify /etc/bashrc on the development machine. FS allows to abstract from the development machine while the production code can continue to use its production paths...

package main

import (
    "errors"
    "fmt"
    "io"
    "io/fs"
    "path/filepath"
    "testing"

    "git.sr.ht/~slukits/fs"
    "git.vizitec.com/goutils/exp"
)

var ErrProduction = errors.New("a production error")

func ProductionFunc(fs *fs.FS) error {
    info, err := fs.Stat("/etc/bashrc")
    if err != nil {
        return fmt.Errorf("%w: %w", ErrProduction, err)
    }
    fl, err := fs.Open("/etc/bashrc")
    if err != nil {
        return fmt.Errorf("%w: %w", ErrProduction, err)
    }
    defer fl.Close()
    _, err = fl.(io.WriterAt).WriteAt(
        []byte("# an appended line\n"), info.Size())
    return err
}

func TestProductionFuncErrorBehavior(t *testing.T) {
    sysFS, brc := new(fs.FS), "/etc/bashrc"
    info, err := sysFS.Stat(brc)
    exp := exp.ErrIs(t, err, nil)
    bb, err := sysFS.ReadFile(brc)
    exp.ErrIs(err, nil)
    memFs := test.FS(t, test.Dir("/etc"), fs.WD("/etc"))
    err = memFs.WriteFile(brc, bb, info.Mode().Perm())
    exp.ErrIs(err, nil)
    t.Log(memFs.String())
    exp.ErrIs(ProductionFunc(memFs.ProductionFS), stdFS.ErrPermission)
}

Above test prints out

=== RUN   TestProductionFuncErrorBehavior
    /Users/goedel/go/src/vizitec/goutils/dir/testdata/demo/main_test.go:51: [-r--r--r-- 265 2024-05-21 10:39:07 bashrc]
--- PASS: TestProductionFuncErrorBehavior (0.00s)

I.e. the modification rights got preserved leading to an fs.ErrPermission-error. As far as I have seen testing.MapFS wouldn't report that error. Note in an in memory filesystem the runtime is always considered the owner of all directories and files, i.e. user related modification permissions testing is not possible out of the box. On the other hand basic permission testing like the above is implemented by the in memory file system. More precisely the in memory file system checks if a directory can be accessed by its owner and if the owner modification permissions are sufficient for a requested file system operation.

using the testing backend

While the fs.FS-api already comes with everything needed for testing fs.FS' testing backend has still a few more conveniences like a pretty fast way to create directory/file structures:

package main

import (
    "testing"

    "git.sr.ht/~slukits/fs/testdata/test"
)

func TestProductionFuncErrorBehavior(t *testing.T) {
    fx := test.FS(t,
        test.Dir("d_a", test.FF("daf1", "daf2", "daf3")),
        test.Dir("d_b"),
        test.Dir("d_c", test.Dir("d_d",
            test.Dir("d_e", test.FF("def1", "def2")))),
    )
    t.Log(fx.String())
}

This will print to stdout

d_a (drwxr-xr-x)
    daf1 (-rw-r--r--)
    daf2 (-rw-r--r--)
    daf3 (-rw-r--r--)
d_b (drwxr-xr-x)
d_c (drwxr-xr-x)
    d_d (drwxr-xr-x)
        d_e (drwxr-xr-x)
            def1 (-rw-r--r--)
            def2 (-rw-r--r--)

I.e. an "goutils/dir/testdata/test".FSFixture has also a convenient way to check what the file system fixture contains. Having created this filesystem we might want to use it for several tests:

package main

import (
    "testing"

    "git.sr.ht/~slukits/fs"
    "git.sr.ht/~slukits/fs/testdata/test"
)

var goldenFS = func() *fs.FS {
    return test.FS(&testing.T{}, test.NotParallel(),
        test.Dir("d_a", test.FF("daf1", "daf2", "daf3")),
        test.Dir("d_b"),
        test.Dir("d_c", test.Dir("d_d",
            test.Dir("d_e", test.FF("def1", "def2")))),
    ).ProductionFS
}()

func TestProductionFuncErrorBehavior(t *testing.T) {
    fx := test.FSFromCopy(t, goldenFS)
    t.Log(fx.String())
}

That &testing.T{}, test.NotParallel() is needed is probably worth improving. Maybe having a test.GoldenFS constructor which works without an mandatory *testing.T instance but panics instead. Anyway executing this test prompts the same output as above. Above created files have a default content. If content matters it's probably more common to create a golden directory containing the file structure we need. In below example there is a "testdata/goldenfs" directory with above file structure. I.e.

package main_test

import (
    "path/filepath"
    "runtime"
    "testing"

    "git.sr.ht/~slukits/fs"
    "git.sr.ht/~slukits/fs/testdata/test"
)

func TestCopyGoldenFileStructure(t *testing.T) {
    _, f, _, ok := runtime.Caller(0)
    if !ok {
        t.Fatalf("can't obtain testdata directory")
    }
    golden := filepath.Join(filepath.Dir(f), "testdata", "goldenfs")
    src := new(fs.FS)
    src.Apply(fs.WD(golden))
    fx := test.FSFromCopy(t, src)
    t.Log("\n", fx.WD())
    t.Log(fx.String())
}

provides still the same output

=== RUN   TestCopyGoldenFileStructure
=== PAUSE TestCopyGoldenFileStructure
=== CONT  TestCopyGoldenFileStructure
/Users/goedel/go/src/vizitec/goutils/dir/testdata/demo/h_golden/main_test.go:21:
/Users/goedel/go/src/vizitec/goutils/dir/testdata/demo/h_golden/testdata/goldenfs
/Users/goedel/go/src/vizitec/goutils/dir/testdata/demo/h_golden/main_test.go:22:
d_a (drwxr-xr-x)
        daf1 (-rw-r--r--)
        daf2 (-rw-r--r--)
        daf3 (-rw-r--r--)
d_b (drwxr-xr-x)
d_c (drwxr-xr-x)
        d_d (drwxr-xr-x)
            d_e (drwxr-xr-x)
                def1 (-rw-r--r--)
                def2 (-rw-r--r--)

--- PASS: TestCopyGoldenFileStructure (0.00s)
benchmark in-memory vs tmp on disk

This seems a good point to do an actual benchmark creating a temporary root directory and coping the golden file-structure to it and having an in memory golden-file-structure and copying it to an other in memory file-structure.

package main

import (
    "path/filepath"
    "runtime"
    "testing"

    "git.sr.ht/~slukits/fs"
    "git.sr.ht/~slukits/fs/testdata/test"
    "git.sr.ht/~slukits/exp"
)

var diskGolden = func() *fs.FS {
    _, f, _, ok := runtime.Caller(0)
    if !ok {
        panic("can't obtain testdata directory")
    }
    golden := filepath.Join(filepath.Dir(f), "testdata", "goldenfs")
    dg := new(fs.FS)
    if err := dg.Apply(fs.WD(golden)); err != nil {
        panic(err)
    }
    return dg
}()

var memGolden = func() *fs.FS {
    mg, err := fs.CopyToMem(diskGolden)
    if err != nil {
        panic(err)
    }
    return mg
}()

func TestDiskAndMemEqual(t *testing.T) {
    disk := test.String(t, diskGolden)
    mem := test.String(t, memGolden)
    exp.Eq(t, disk, mem)
}

func BenchmarkCopyInMemoryFileStructure(b *testing.B) {
    for n := 0; n < b.N; n++ {
        snk := new(fs.FS)
        err := snk.Apply(fs.MemFS(), fs.WD(fs.SystemRoot))
        if err != nil {
            b.Fatal(err)
        }
        if err := fs.Copy(memGolden, snk); err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkCopyOnDiskFileStructure(b *testing.B) {
    for n := 0; n < b.N; n++ {
        snk := new(fs.FS)
        if err := snk.Apply(fs.WD(b.TempDir())); err != nil {
            b.Fatal(err)
        }
        if err := fs.Copy(diskGolden, snk); err != nil {
            b.Fatal(err)
        }
    }
}

On the same machine copying the file-structure from the golden directory to the temporary data structure is almost two magnitudes slower than the in memory copy even for such a small file structure where we can expect that reading the files from the disk in the benchmark will most likely not hit the disk but some caching.

i_bench % go test -bench ...
goos: darwin
goarch: arm64
pkg: git.sr.ht/~slukits/fs/testdata/demo/i_bench
BenchmarkCopyInMemoryFileStructure-8      136958              8663 ns/op
BenchmarkCopyOnDiskFileStructure-8          2101            563092 ns/op
PASS
ok      git.sr.ht/~slukits/fs/testdata/demo/i_bench       3.480s
composing the testing backend into others

Suppose the creation of a testing backend of an other type MyType in the package mypkg which uses a file-system (see dir/testdata/demo/j_compose for a fully working example). Our goal is now that this type uses a fs.FS instance which is the production instance of a "dir/testdata/test".FSFixture instance and that we can use options of dir/testdata/test in the constructor of the test.MyTypeFixture:

// j_compose/mytype_test.go (Note the package is not named after the dir)
package mypkg_test

import (
    . "testing"

    "git.sr.ht/~slukits/fs/testdata/demo/j_compose/testdata/test"
    tstDir "git.sr.ht/~slukits/fs/testdata/test"
)

const expContent = `
d_a (drwxr-xr-x)
    daf1 (-rw-r--r--)
    daf2 (-rw-r--r--)
`

func TestTestingBackendHasMyTypesFileSystem(t *T) {
    fx := test.MyType(t, tstDir.Dir("d_a", tstDir.FF("daf1", "daf2")))
    fx.Exp.Eq(fx.FS.ProductionFS, fx.ProductionMyType.FS())
    fx.Exp.Eq(fx.FS.String(), expContent)
}

I.e. fx.FS in above code is the "dir/testdata/test".FSFixture instance. The testing backend of MyType might looks like this

// j_compose/testdata/test/fixture.go
package test

import (
    "testing"

    mypkg "git.sr.ht/~slukits/fs/testdata/demo/j_compose"
    tstDir "git.sr.ht/~slukits/fs/testdata/test"
    "git.sr.ht/~slukits/eventsourced/pkg/settings"
    "git.sr.ht/~slukits/exp"
)

type (
    ProductionMyType = *mypkg.MyType
    t                = *testing.T
)

type MyTypeFixture struct {
    t
    ProductionMyType
    FS  *tstDir.FSFixture
    Exp *exp.Exp
}

func MyType(t *testing.T, oo ...settings.Option) *MyTypeFixture {
    if t == nil {
        panic("fx: my type: given *testing.T instance must not be zero")
    }
    fx := &MyTypeFixture{t: t,
        FS:               new(tstFS.FSFixture),
        ProductionMyType: new(mypkg.MyType),
    }
    oo = append([]settings.Option{
        mypkg.RegisterMyType(fx.ProductionMyType),
        tstFS.RegisterFS(t, fx.FS),
    }, oo...)
    occ := new(settings.Contexts)
    occ.Finalize(func(c *settings.Contexts) error {
       // the production fs access needs to be delayed
       // until it is ensured that it is set
       return mypkg.SetFS(fx.FS.ProductionFS)(occ)
    })
    if err := occ.Apply(oo...); err != nil {
        fx.Fatalf("fx: my type: apply options: %v", err)
    }
    fx.Exp = &exp.Exp{T: t}
    return fx
}

To be able to apply mypkg.SetFS after composing the type instances we need to have a (private) options context for used MyType-instance. This options-context is registered using the RegisterMyType-option mypkg.RegisterMyType. I.e. a minimal implementation of MyType may look like this:

// j_compose/mytype.go
package mypkg

import "git.sr.ht/~slukits/fs"

type MyType struct {
    fs          *fs.FS
    initialized bool
}

func (mt *MyType) ensureInitialization() {
    if mt.initialized {
        return
    }
    mt.initialized = true
    if mt.fs == nil {
        mt.fs = new(fs.FS)
    }
}

func (mt *MyType) FS() *fs.FS {
    return mt.fs
}

along with its options-aspects:

// j_compose/options.go
package mypkg

import (
    "git.sr.ht/~slukits/fs"
    "git.sr.ht/~slukits/eventsourced/pkg/settings"
    "github.com/google/uuid"
)

var myTypeOptionsContextID = uuid.New().String()

func RegisterMyType(mt *MyType) settings.Option {
    return func(occ *settings.Contexts) error {
        return occ.Register(myTypeOptionsContextID, mt)
    }
}

func otx(occ *settings.Contexts) *MyType {
    return occ.Get(myTypeOptionsContextID).(*MyType)
}

func SetFS(fs *fs.FS) settings.Option {
    return func(occ *settings.Contexts) error {
        otx(occ).fs = fs
        return nil
    }
}

Which makes the introducing test of this section work.

Documentation

Index

Constants

View Source
const MemWDMode = 0777

Variables

View Source
var AdaptFS = struct {
	Open      func(func(FnOpen) FnOpen) settings.Setting
	Append    func(func(FnAppend) FnAppend) settings.Setting
	ReadFile  func(func(FnReadFile) FnReadFile) settings.Setting
	Stat      func(func(FnStat) FnStat) settings.Setting
	ReadDir   func(func(FnReadDir) FnReadDir) settings.Setting
	WriteFile func(func(FnWriteFile) FnWriteFile) settings.Setting
	MkdirAll  func(func(FnMkdirAll) FnMkdirAll) settings.Setting
	RemoveAll func(func(FnRemoveAll) FnRemoveAll) settings.Setting
	IsInMem   func(func(FnIsInMem) FnIsInMem) settings.Setting
}{
	func(fn func(FnOpen) FnOpen) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.open = fn(fs.open)
			return nil
		}
	},
	func(fn func(FnAppend) FnAppend) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.append = fn(fs.append)
			return nil
		}
	},
	func(fn func(FnReadFile) FnReadFile) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.readFile = fn(fs.readFile)
			return nil
		}
	},
	func(fn func(FnStat) FnStat) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.stat = fn(fs.stat)
			return nil
		}
	},
	func(fn func(FnReadDir) FnReadDir) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.readDir = fn(fs.readDir)
			return nil
		}
	},
	func(fn func(FnWriteFile) FnWriteFile) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.writeFile = fn(fs.writeFile)
			return nil
		}
	},
	func(fn func(FnMkdirAll) FnMkdirAll) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.mkdirAll = fn(fs.mkdirAll)
			return nil
		}
	},
	func(fn func(FnRemoveAll) FnRemoveAll) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.rmAll = fn(fs.rmAll)
			return nil
		}
	},
	func(fn func(FnIsInMem) FnIsInMem) settings.Setting {
		return func(occ *settings.Contexts) error {
			fs := otx(occ)
			fs.ensureInitialization()
			fs.isInMem = fn(fs.isInMem)
			return nil
		}
	},
}

AdaptFS provides methods to provide options to adapt a file system's methods. NOTE if you want to adapt the methods of an in-memory file system be sure to apply first the fs.MemFS-option before applying any of the AdaptFS-options.

View Source
var ErrFlag = errors.New("fs: mem: open file: given flags are not supported")
View Source
var ErrRoot = errors.New("fs: mem: system root can't be removed")
View Source
var ErrUnrooted = errors.New("mem-fs expects file/dir to be " +
	"rooted if no working-dir set")
View Source
var ErrWDMissing = errors.New("expect file system to copy to have " +
	"a working dir")
View Source
var ErrWDSystemRoot = errors.New("expect file system to copy to " +
	"have a more specific working dir than system root")
View Source
var ErrWriteAtInAppendMode = errors.New("os: invalid use of WriteAt on file opened with O_APPEND")
View Source
var (
	RootedErr = "dir: FS: set rooted working directory"
)
View Source
var SystemRoot = func() string {
	return os.Getenv("SystemDrive") + string(os.PathSeparator)
}()

SystemRoot is an os independent root of an os file system.

View Source
var SystemRootLower = func() string {
	return strings.ToLower(SystemRoot)
}()

SystemRoot is an os independent root of an os file system converted to lower runes e.g.: C:\ => c:\.

Functions

func ApplyOnly

func ApplyOnly(fls *FS, ss ...settings.Setting) error

ApplyOnly given options oo to given file system instance fls. ApplyOnly fails if one of its option-applications fails.

func ConvertExpToTesting

func ConvertExpToTesting(ll lines) lines

ConvertExpToTesting converts the exp.ASSERT(t, ...) lines of a test function to a exp := testing.ASSERT(t, ...) and exp.ASSERT(...) lines.

func Copy

func Copy(fsSrc, fsSnk *FS) error

Copy regular files with their directories from file system fsSrc to the file system fsSnk. Copy fails if fsSrc is not in memory but has no working directory or fs.SystemRoot as working directory set. (Just as precaution; if needed an option could be added preventing this check.) It fails also if one of the file system operations on either the source or the sink fails.

func IsEqualFileInfo

func IsEqualFileInfo(fi, other fs.FileInfo) bool

IsEqualFileInfo returns true if given file info fi's method values of IsDir, Mode, Name, Size and ModTime are equal to other given file info's corresponding method value.

func IsRoot

func IsRoot(path string) bool

IsRoot return true if path is either SystemRootLower or SystemRoot.

func IsRooted

func IsRooted(path string) bool

IsRooted returns true if given path is either prefixed by fs.SystemRoot or by fs.SystemRootLower

func MemFS

func MemFS() settings.Setting

func Register

func Register(fs *FS, oo ...settings.Setting) settings.Setting

Register registers given filesystem instance fs at the options contexts that apply returned option which fails if that options contexts have already an options context for a FS instance registered.

func TrimRoot

func TrimRoot(path string) string

func WD

func WD(rd string) settings.Setting

WD sets given rooted directory of the fs.FS instance returned option is applied to. WD fails if rd is not prefixed by fs.SystemRoot.

Types

type FS

type FS struct {
	// contains filtered or unexported fields
}

FS is an adaptable (mockable) abstraction of an file system. Compared to "io/fs" it allows for absolute paths, i.e. new(FS).ReadFile('/Users/me/a/file') will not fail if the path exists on the system. It allows for windows paths, one can write to a path and one can adapt an FS-instance's methods. The later feature is also used internally to provide an in-memory file system see fs.MemFS.

func Apply

func Apply(fls *FS, ss ...settings.Setting) (*FS, error)

Apply given options oo to given file system instance fls. Apply fails if one of its option-applications fails and returns otherwise fls to use Apply as sort of an constructor:

fls, err := fs.Apply(new(fs.FS), fs.MemFS)

func CopyToMem

func CopyToMem(fs *FS, ss ...settings.Setting) (*FS, error)

CopyToMem expects given file system fs to be rooted to a directory more specific than the fs.SystemRoot or to be an in-memory fs and returns an in-memory copy as long as fs only contains directories and regular files. Given options are applied to the copy of fs.

func (*FS) Append

func (fs *FS) Append(name string, perm os.FileMode) (fs.File, error)

func (*FS) IsInMemory

func (fs *FS) IsInMemory() bool

func (*FS) MkdirAll

func (fs *FS) MkdirAll(name string, perm fs.FileMode) error

func (*FS) Open

func (fs *FS) Open(name string) (fs.File, error)

Open file with given name in given file system fs. File system fails with fs.NotExists if name doesn't exist in fs otherwise a fs.File instance is returned. TODO: the mem-implementation allows als to write to an opened file while the default os.Open does not. It is planned to make them behave equally. But then we don't have any way to change a file. I.e. we need to add an OpenRW-method if we want to avoid to implement all the possible flags and their combination.

func (*FS) ReadDir

func (fs *FS) ReadDir(name string) ([]fs.DirEntry, error)

func (*FS) ReadFile

func (fs *FS) ReadFile(name string) ([]byte, error)

func (*FS) ReadFromParent

func (fls *FS) ReadFromParent(dir, name string) ([]byte, error)

func (*FS) RemoveAll

func (fs *FS) RemoveAll(name string) error

func (*FS) Stat

func (fs *FS) Stat(name string) (fs.FileInfo, error)

func (*FS) WD

func (fs *FS) WD() string

WD returns a set rooted working directory which defaults to the os' working directory if fs is not an in-memory fs otherwise it defaults zero. In the later case case names are expected to be absolute paths otherwise names that are not prefixed by fs.SystemRoot are interpreted relative to WD.

func (*FS) WriteFile

func (fs *FS) WriteFile(
	name string, data []byte, perm fs.FileMode,
) error

type FnAppend

type FnAppend func(string, os.FileMode) (fs.File, error)

type FnIsInMem

type FnIsInMem func() bool

type FnMkdirAll

type FnMkdirAll func(string, fs.FileMode) error

type FnOpen

type FnOpen func(string) (fs.File, error)

type FnReadDir

type FnReadDir func(string) ([]fs.DirEntry, error)

type FnReadFile

type FnReadFile func(string) ([]byte, error)

type FnRemoveAll

type FnRemoveAll func(string) error

type FnStat

type FnStat func(string) (fs.FileInfo, error)

type FnWriteFile

type FnWriteFile func(string, []byte, fs.FileMode) error

type MemModTimeSetter

type MemModTimeSetter interface {
	SetModTime(time.Time)
}

MemModTimeSetter is an fs.FileInfo extension that allows to set the a files modification time.

type MemPermissionSetter

type MemPermissionSetter interface {
	SetPerm(fs.FileMode)
}

MemPermissionSetter is an fs.FileInfo extension that allows to set the a files permission bits.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL