diff --git a/.vscode/settings.json b/.vscode/settings.json index dc052f5..5d5522c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,7 @@ "lorax", "Marillion", "mattn", + "msys", "musico", "Mutables", "nakedret", @@ -70,6 +71,7 @@ "Persistables", "pixa", "prealloc", + "qsys", "repotoken", "rsys", "samber", diff --git a/builders.go b/builders.go index 15bceb4..885a493 100644 --- a/builders.go +++ b/builders.go @@ -19,17 +19,22 @@ type buildArtefacts struct { } type Builders struct { - filesystem pref.FileSystemBuilder - options optionsBuilder - navigator kernel.NavigatorBuilder - plugins pluginsBuilder - extent extentBuilder + readerFS pref.ReadDirFileSystemBuilder + queryFS pref.QueryStatusFileSystemBuilder + options optionsBuilder + navigator kernel.NavigatorBuilder + plugins pluginsBuilder + extent extentBuilder } func (bs *Builders) buildAll() (*buildArtefacts, error) { // BUILD FILE SYSTEM & EXTENT // - ext := bs.extent.build(bs.filesystem.Build()) + reader := bs.readerFS.Build() + ext := bs.extent.build( + reader, + bs.queryFS.Build(reader), + ) // BUILD OPTIONS // @@ -56,8 +61,9 @@ func (bs *Builders) buildAll() (*buildArtefacts, error) { } artefacts, navErr := bs.navigator.Build(o, &types.Resources{ - FS: types.FileSystems{ + FS: FileSystems{ N: ext.navFS(), + Q: ext.queryFS(), R: ext.resFS(), }, Supervisor: measure.New(), diff --git a/core/hooks.go b/core/hooks.go index 9514d34..0cafeb6 100644 --- a/core/hooks.go +++ b/core/hooks.go @@ -7,12 +7,12 @@ import ( type ( // QueryStatusHook function signature that enables the default to be overridden. // (By default, uses Lstat) - QueryStatusHook func(path string) (fs.FileInfo, error) + QueryStatusHook func(qsys fs.StatFS, path string) (fs.FileInfo, error) // ReadDirectoryHook hook function to define implementation of how a directory's // entries are read. A default implementation is preset, so does not have to be set // by the client. - ReadDirectoryHook func(sys fs.FS, dirname string) ([]fs.DirEntry, error) + ReadDirectoryHook func(rsys fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) // SortHook hook function to define how directory entries are sorted. Does not // have to be set explicitly. This will be set according to the IsCaseSensitive on diff --git a/director-resume_test.go b/director-resume_test.go index a31c33f..bdd5756 100644 --- a/director-resume_test.go +++ b/director-resume_test.go @@ -2,7 +2,10 @@ package tv_test import ( "context" + "io/fs" + "os" "sync" + "testing/fstest" "github.com/fortytw2/leaktest" . "github.com/onsi/ginkgo/v2" //nolint:revive // ok @@ -16,7 +19,10 @@ import ( ) var _ = Describe("Director(Resume)", Ordered, func() { - var restore pref.Option + var ( + emptyFS fstest.MapFS + restore pref.Option + ) BeforeAll(func() { restore = func(o *tv.Options) error { @@ -24,6 +30,11 @@ var _ = Describe("Director(Resume)", Ordered, func() { return nil } + emptyFS = fstest.MapFS{ + ".": &fstest.MapFile{ + Mode: os.ModeDir, + }, + } }) BeforeEach(func() { @@ -45,6 +56,12 @@ var _ = Describe("Director(Resume)", Ordered, func() { Using: tv.Using{ Subscription: tv.SubscribeFiles, Handler: noOpHandler, + GetReadDirFS: func() fs.ReadDirFS { + return emptyFS + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { + return emptyFS + }, }, From: RestorePath, Strategy: tv.ResumeStrategyFastward, diff --git a/director.go b/director.go index 47823a4..042ce04 100644 --- a/director.go +++ b/director.go @@ -2,7 +2,6 @@ package tv import ( "io/fs" - "os" "github.com/snivilised/traverse/internal/hiber" "github.com/snivilised/traverse/internal/kernel" @@ -62,17 +61,26 @@ func Prime(using *pref.Using, settings ...pref.Option) *Builders { // by a panic. // return &Builders{ - filesystem: pref.FileSystem(func() fs.FS { - if using.GetFS != nil { - return using.GetFS() + readerFS: pref.CreateReadDirFS(func() fs.ReadDirFS { + if using.GetReadDirFS != nil { + return using.GetReadDirFS() } - return os.DirFS(using.Root) + + return NewNativeFS(using.Root) + }), + queryFS: pref.CreateQueryStatusFS(func(qsys fs.FS) fs.StatFS { + if using.GetQueryStatusFS != nil { + return using.GetQueryStatusFS(qsys) + } + + return NewQueryStatusFS(qsys) }), - extent: extension(func(fsys fs.FS) extent { + extent: extension(func(rsys fs.ReadDirFS, qsys fs.StatFS) extent { return &primeExtent{ baseExtent: baseExtent{ - fsys: fileSystems{ - nas: fsys, + fileSys: fileSystems{ + nas: rsys, + qus: qsys, }, }, u: using, @@ -109,17 +117,25 @@ func Resume(was *Was, settings ...pref.Option) *Builders { // path was. // return &Builders{ - filesystem: pref.FileSystem(func() fs.FS { - if was.Using.GetFS != nil { - return was.Using.GetFS() + readerFS: pref.CreateReadDirFS(func() fs.ReadDirFS { + if was.Using.GetReadDirFS != nil { + return was.Using.GetReadDirFS() } - return os.DirFS(was.Using.Root) + return NewNativeFS(was.Root) + }), + queryFS: pref.CreateQueryStatusFS(func(fsys fs.FS) fs.StatFS { + if was.Using.GetQueryStatusFS != nil { + return was.Using.GetQueryStatusFS(fsys) + } + + return NewQueryStatusFS(fsys) }), - extent: extension(func(fsys fs.FS) extent { + extent: extension(func(rsys fs.ReadDirFS, qsys fs.StatFS) extent { return &resumeExtent{ baseExtent: baseExtent{ - fsys: fileSystems{ - nas: fsys, + fileSys: fileSystems{ + nas: rsys, + qus: qsys, }, }, w: was, diff --git a/extent.go b/extent.go index 00bbb0f..b5c4344 100644 --- a/extent.go +++ b/extent.go @@ -14,26 +14,32 @@ type extent interface { was() *pref.Was plugin(*kernel.Artefacts) types.Plugin options(...pref.Option) (*pref.Options, error) - navFS() fs.FS + navFS() fs.ReadDirFS + queryFS() fs.StatFS resFS() fs.FS complete() bool } type fileSystems struct { - nas fs.FS + nas fs.ReadDirFS + qus fs.StatFS res fs.FS } type baseExtent struct { - fsys fileSystems + fileSys fileSystems } -func (ex *baseExtent) navFS() fs.FS { - return ex.fsys.nas +func (ex *baseExtent) navFS() fs.ReadDirFS { + return ex.fileSys.nas +} + +func (ex *baseExtent) queryFS() fs.StatFS { + return ex.fileSys.qus } func (ex *baseExtent) resFS() fs.FS { - return ex.fsys.nas + return ex.fileSys.nas } type primeExtent struct { @@ -88,7 +94,7 @@ func (ex *resumeExtent) plugin(artefacts *kernel.Artefacts) types.Plugin { } func (ex *resumeExtent) options(settings ...pref.Option) (*pref.Options, error) { - loaded, err := resume.Load(ex.fsys.res, ex.w.From, settings...) + loaded, err := resume.Load(ex.fileSys.res, ex.w.From, settings...) ex.loaded = loaded // get the resume point from the resume persistence file diff --git a/internal-traverse-defs.go b/internal-traverse-defs.go index c835710..ee3ab6b 100644 --- a/internal-traverse-defs.go +++ b/internal-traverse-defs.go @@ -52,13 +52,13 @@ func (fn filesystem) build(path string) fs.FS { } type extentBuilder interface { - build(fsys fs.FS) extent + build(rsys fs.ReadDirFS, qsys fs.StatFS) extent } -type extension func(fs.FS) extent +type extension func(rsys fs.ReadDirFS, qsys fs.StatFS) extent -func (fn extension) build(fsys fs.FS) extent { - return fn(fsys) +func (fn extension) build(rsys fs.ReadDirFS, qsys fs.StatFS) extent { + return fn(rsys, qsys) } // We need an entity that manages the decoration of the client handler. The diff --git a/internal/helpers/directory-tree-builder.go b/internal/helpers/directory-tree-builder.go index 322a3c1..e4eb66b 100644 --- a/internal/helpers/directory-tree-builder.go +++ b/internal/helpers/directory-tree-builder.go @@ -22,15 +22,15 @@ const ( doWrite = true ) -func Musico(verbose bool, portions ...string) (fsys fstest.MapFS, root string) { - fsys = fstest.MapFS{ +func Musico(verbose bool, portions ...string) (msys fstest.MapFS, root string) { + msys = fstest.MapFS{ ".": &fstest.MapFile{ Mode: os.ModeDir, }, } - return fsys, Provision( - NewMemWriteProvider(fsys, os.ReadFile, portions...), + return msys, Provision( + NewMemWriteProvider(msys, os.ReadFile, portions...), verbose, portions..., ) diff --git a/internal/kernel/navigation-fs.go b/internal/kernel/navigation-fs.go index 16a5078..8e069ab 100644 --- a/internal/kernel/navigation-fs.go +++ b/internal/kernel/navigation-fs.go @@ -4,7 +4,7 @@ import ( "io/fs" ) -func read(sys fs.FS, o *readOptions, path string) (*Contents, error) { +func read(sys fs.ReadDirFS, o *readOptions, path string) (*Contents, error) { entries, err := o.hooks.read.Invoke()(sys, path) contents := newContents( diff --git a/internal/kernel/navigator-agent.go b/internal/kernel/navigator-agent.go index 9bac113..7f4054a 100644 --- a/internal/kernel/navigator-agent.go +++ b/internal/kernel/navigator-agent.go @@ -46,7 +46,10 @@ func (n *navigatorAgent) Ignite(ignition *types.Ignition) { func (n *navigatorAgent) top(ctx context.Context, ns *navigationStatic, ) (*types.KernelResult, error) { - info, ie := n.ao.hooks.QueryStatus.Invoke()(ns.root) + info, ie := n.ao.hooks.QueryStatus.Invoke()( + ns.mediator.resources.FS.Q, ns.root, + ) + err := lo.TernaryF(ie != nil, func() error { return n.ao.defects.Fault.Accept(&pref.NavigationFault{ diff --git a/internal/kernel/navigator-filter-custom_test.go b/internal/kernel/navigator-filter-custom_test.go index cd9ed04..ad30470 100644 --- a/internal/kernel/navigator-filter-custom_test.go +++ b/internal/kernel/navigator-filter-custom_test.go @@ -71,17 +71,24 @@ var _ = Describe("NavigatorFilterCustom", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: callback, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.WithFilterCustom(customFilter), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), )).Navigate(ctx) assertFilteredNavigation(entry, testOptions{ diff --git a/internal/kernel/navigator-filter-extended-glob_test.go b/internal/kernel/navigator-filter-extended-glob_test.go index d612f2e..f8cbc87 100644 --- a/internal/kernel/navigator-filter-extended-glob_test.go +++ b/internal/kernel/navigator-filter-extended-glob_test.go @@ -78,20 +78,29 @@ var _ = Describe("NavigatorFoldersWithFiles", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: callback, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.WithFilter(filterDefs), - tv.WithFilterSink(func(reply pref.FilterReply) { - traverseFilter = reply.Node - }), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithFilterSink( + func(reply pref.FilterReply) { + traverseFilter = reply.Node + }, + ), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), )).Navigate(ctx) assertFilteredNavigation(entry, testOptions{ diff --git a/internal/kernel/navigator-filter-glob_test.go b/internal/kernel/navigator-filter-glob_test.go index 1c2e0d8..3454059 100644 --- a/internal/kernel/navigator-filter-glob_test.go +++ b/internal/kernel/navigator-filter-glob_test.go @@ -79,20 +79,29 @@ var _ = Describe("NavigatorFilterGlob", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: callback, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.WithFilter(filterDefs), - tv.WithFilterSink(func(reply pref.FilterReply) { - traverseFilter = reply.Node - }), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithFilterSink( + func(reply pref.FilterReply) { + traverseFilter = reply.Node + }, + ), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), )).Navigate(ctx) assertFilteredNavigation(entry, testOptions{ diff --git a/internal/kernel/navigator-filter-regex_test.go b/internal/kernel/navigator-filter-regex_test.go index 111fe15..e5feb18 100644 --- a/internal/kernel/navigator-filter-regex_test.go +++ b/internal/kernel/navigator-filter-regex_test.go @@ -79,20 +79,29 @@ var _ = Describe("NavigatorFilterRegex", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: callback, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.WithFilter(filterDefs), - tv.WithFilterSink(func(reply pref.FilterReply) { - traverseFilter = reply.Node - }), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithFilterSink( + func(reply pref.FilterReply) { + traverseFilter = reply.Node + }, + ), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), )).Navigate(ctx) assertFilteredNavigation(entry, testOptions{ diff --git a/internal/kernel/navigator-folders-with-files-filtered_test.go b/internal/kernel/navigator-folders-with-files-filtered_test.go index 4fef79f..e6c0f2f 100644 --- a/internal/kernel/navigator-folders-with-files-filtered_test.go +++ b/internal/kernel/navigator-folders-with-files-filtered_test.go @@ -73,20 +73,29 @@ var _ = Describe("NavigatorFoldersWithFiles", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: callback, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.WithFilter(filterDefs), - tv.WithFilterSink(func(reply pref.FilterReply) { - childFilter = reply.Child - }), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithFilterSink( + func(reply pref.FilterReply) { + childFilter = reply.Child + }, + ), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), )).Navigate(ctx) Expect(err).To(Succeed()) diff --git a/internal/kernel/navigator-folders-with-files_test.go b/internal/kernel/navigator-folders-with-files_test.go index f51eaf7..fa43e7a 100644 --- a/internal/kernel/navigator-folders-with-files_test.go +++ b/internal/kernel/navigator-folders-with-files_test.go @@ -53,17 +53,24 @@ var _ = Describe("NavigatorFoldersWithFiles", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: once, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.If(entry.caseSensitive, tv.WithHookCaseSensitiveSort()), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), )).Navigate(ctx) assertNavigation(entry, testOptions{ diff --git a/internal/kernel/navigator-simple_test.go b/internal/kernel/navigator-simple_test.go index bda88a3..4fd65d0 100644 --- a/internal/kernel/navigator-simple_test.go +++ b/internal/kernel/navigator-simple_test.go @@ -42,7 +42,7 @@ var _ = Describe("NavigatorUniversal", Ordered, func() { func(ctx SpecContext, entry *naviTE) { recording := make(recordingMap) once := func(node *tv.Node) error { - _, found := recording[node.Path] // should this be name not path? + _, found := recording[node.Path] // TODO: should this be name not path? Expect(found).To(BeFalse()) recording[node.Path] = len(node.Children) @@ -63,18 +63,25 @@ var _ = Describe("NavigatorUniversal", Ordered, func() { Root: path, Subscription: entry.subscription, Handler: callback, - GetFS: func() fs.FS { + GetReadDirFS: func() fs.ReadDirFS { + return vfs + }, + GetQueryStatusFS: func(_ fs.FS) fs.StatFS { return vfs }, }, tv.WithOnBegin(begin("🛡️")), tv.If(entry.caseSensitive, tv.WithHookCaseSensitiveSort()), - tv.WithHookQueryStatus(func(path string) (fs.FileInfo, error) { - return vfs.Stat(helpers.TrimRoot(path)) - }), - tv.WithHookReadDirectory(func(_ fs.FS, dirname string) ([]fs.DirEntry, error) { - return vfs.ReadDir(helpers.TrimRoot(dirname)) - }), + tv.WithHookQueryStatus( + func(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(helpers.TrimRoot(path)) + }, + ), + tv.WithHookReadDirectory( + func(rfs fs.ReadDirFS, dirname string) ([]fs.DirEntry, error) { + return rfs.ReadDir(helpers.TrimRoot(dirname)) + }, + ), ), ).Navigate(ctx) diff --git a/internal/types/definitions.go b/internal/types/definitions.go index d9d15be..f8546b7 100644 --- a/internal/types/definitions.go +++ b/internal/types/definitions.go @@ -8,6 +8,7 @@ import ( "github.com/snivilised/traverse/enums" "github.com/snivilised/traverse/internal/override" "github.com/snivilised/traverse/measure" + "github.com/snivilised/traverse/nfs" "github.com/snivilised/traverse/pref" ) @@ -59,13 +60,9 @@ type ( Supervisor() *measure.Supervisor } - FileSystems struct { - N fs.FS - R fs.FS - } // Resources are dependencies required for navigation Resources struct { - FS FileSystems + FS nfs.FileSystems Supervisor *measure.Supervisor Actions *override.Actions } diff --git a/nfs/file-systems.go b/nfs/file-systems.go new file mode 100644 index 0000000..71f9f16 --- /dev/null +++ b/nfs/file-systems.go @@ -0,0 +1,42 @@ +package nfs + +import ( + "io/fs" + "os" +) + +type nativeFS struct { + fsys fs.FS +} + +func NewNativeFS(path string) fs.ReadDirFS { + return &nativeFS{ + fsys: os.DirFS(path), + } +} + +func (n *nativeFS) Open(path string) (fs.File, error) { + return n.fsys.Open(path) +} + +func (n *nativeFS) ReadDir(name string) ([]fs.DirEntry, error) { + return fs.ReadDir(n.fsys, name) +} + +type queryStatusFS struct { + fsys fs.FS +} + +func NewQueryStatusFS(fsys fs.FS) fs.StatFS { + return &queryStatusFS{ + fsys: fsys, + } +} + +func (q *queryStatusFS) Open(name string) (fs.File, error) { + return q.fsys.Open(name) +} + +func (q *queryStatusFS) Stat(name string) (fs.FileInfo, error) { + return os.Stat(name) +} diff --git a/nfs/nfs-defs.go b/nfs/nfs-defs.go new file mode 100644 index 0000000..9f6271e --- /dev/null +++ b/nfs/nfs-defs.go @@ -0,0 +1,30 @@ +package nfs + +import ( + "io/fs" +) + +// nfs contains file system abstractions for navigation. Since +// there are no standard write-able file system interfaces, +// we need to define proprietary ones here in this package. +// This is a low level package that should not use anything else in +// traverse. + +type ( + // FileSystems contains the logical file systems required + // for navigation. + FileSystems struct { + // N represents the read only navigation file system. Uses + // of the shelf interface as defined by the standard library. + N fs.ReadDirFS + + // Q represents the file system instance that can perform a query + // status via an Lstat call. + Q fs.StatFS + + // R represents the resume/save file system that requires + // write access and whose path should be outside of the path + // represented by N, the navigation file system. + R fs.FS + } +) diff --git a/pref/defaults.go b/pref/defaults.go index b0cc8b1..6422aed 100644 --- a/pref/defaults.go +++ b/pref/defaults.go @@ -13,7 +13,9 @@ import ( // DefaultReadEntriesHook reads the contents of a directory. The resulting // slice is left un-sorted -func DefaultReadEntriesHook(sys fs.FS, dirname string) ([]fs.DirEntry, error) { +func DefaultReadEntriesHook(sys fs.ReadDirFS, + dirname string, +) ([]fs.DirEntry, error) { const all = -1 contents, err := fs.ReadDir(sys, dirname) @@ -26,6 +28,12 @@ func DefaultReadEntriesHook(sys fs.FS, dirname string) ([]fs.DirEntry, error) { }), nil } +// DefaultQueryStatusHook query the status of the path on the file system +// provided. +func DefaultQueryStatusHook(qsys fs.StatFS, path string) (fs.FileInfo, error) { + return qsys.Stat(path) +} + // CaseSensitiveSortHook hook function for case sensitive directory traversal. A // directory of "a" will be visited after a sibling directory "B". func CaseSensitiveSortHook(entries []fs.DirEntry, _ ...any) { @@ -84,6 +92,8 @@ func DefaultPanicHandler() { // may this should invoke save } -func DefaultSkipHandler(*core.Node, core.DirectoryContents, error) (enums.SkipTraversal, error) { +func DefaultSkipHandler(*core.Node, + core.DirectoryContents, error, +) (enums.SkipTraversal, error) { return enums.SkipNoneTraversal, nil } diff --git a/pref/options.go b/pref/options.go index 869d100..e499760 100644 --- a/pref/options.go +++ b/pref/options.go @@ -3,7 +3,6 @@ package pref import ( "io/fs" "log/slog" - "os" "runtime" "github.com/snivilised/traverse/core" @@ -152,7 +151,7 @@ func DefaultOptions() *Options { FileSubPath: tapable.NewHookCtrl[core.SubPathHook](RootParentSubPathHook), FolderSubPath: tapable.NewHookCtrl[core.SubPathHook](RootParentSubPathHook), ReadDirectory: tapable.NewHookCtrl[core.ReadDirectoryHook](DefaultReadEntriesHook), - QueryStatus: tapable.NewHookCtrl[core.QueryStatusHook](os.Lstat), + QueryStatus: tapable.NewHookCtrl[core.QueryStatusHook](DefaultQueryStatusHook), Sort: tapable.NewHookCtrl[core.SortHook](CaseInSensitiveSortHook), }, diff --git a/pref/using.go b/pref/using.go index 0ede1c3..d780048 100644 --- a/pref/using.go +++ b/pref/using.go @@ -31,9 +31,15 @@ type Using struct { // property. O *Options - // GetFS is optional and enables the client to specify how the + // GetReadDirFS is optional and enables the client to specify how the // file system for a path is created - GetFS FileSystem + GetReadDirFS CreateReadDirFS + + // GetQueryStatusFS is optional and enables the client to specify how the + // file system for a path is created. When specified can probably use + // the same instance used to create the ReadDi fs, that is because + // fstest.MapFS implements the required method Stat. + GetQueryStatusFS CreateQueryStatusFS } // Validate checks that the properties on Using are all valid. @@ -62,7 +68,7 @@ type Was struct { } // Validate checks that the properties on Using and Was are all valid. -func (a Was) Validate() error { +func (a Was) Validate() error { //nolint:gocritic // heavy, so what, low frequency if a.From == "" { return UsageError{ message: "missing restore from path", @@ -94,12 +100,26 @@ func validate(using *Using) error { return nil } -type FileSystemBuilder interface { - Build() fs.FS -} +type ( + ReadDirFileSystemBuilder interface { + Build() fs.ReadDirFS + } -type FileSystem func() fs.FS + CreateReadDirFS func() fs.ReadDirFS +) -func (fn FileSystem) Build() fs.FS { +func (fn CreateReadDirFS) Build() fs.ReadDirFS { return fn() } + +type ( + QueryStatusFileSystemBuilder interface { + Build(fsys fs.FS) fs.StatFS + } + + CreateQueryStatusFS func(fsys fs.FS) fs.StatFS +) + +func (fn CreateQueryStatusFS) Build(fsys fs.FS) fs.StatFS { + return fn(fsys) +} diff --git a/tapable/tapable_test.go b/tapable/tapable_test.go index df705db..453cb4b 100644 --- a/tapable/tapable_test.go +++ b/tapable/tapable_test.go @@ -3,10 +3,12 @@ package tapable_test import ( "io/fs" "os" + "testing/fstest" . "github.com/onsi/ginkgo/v2" //nolint:revive // ok . "github.com/onsi/gomega" //nolint:revive // ok + tv "github.com/snivilised/traverse" "github.com/snivilised/traverse/core" "github.com/snivilised/traverse/pref" ) @@ -27,8 +29,17 @@ var _ = Describe("Tapable", Ordered, func() { invoked bool o *pref.Options err error + emptyFS fstest.MapFS ) + BeforeAll(func() { + emptyFS = fstest.MapFS{ + ".": &fstest.MapFile{ + Mode: os.ModeDir, + }, + } + }) + BeforeEach(func() { invoked = false o, err = pref.Get() @@ -64,11 +75,12 @@ var _ = Describe("Tapable", Ordered, func() { When("ReadDirectory hooked", func() { It("🧪 should: invoke hook", func() { - sys := os.DirFS(root) - o.Hooks.ReadDirectory.Tap(func(_ fs.FS, _ string) ([]fs.DirEntry, error) { + o.Hooks.ReadDirectory.Tap(func(_ fs.ReadDirFS, _ string) ([]fs.DirEntry, error) { invoked = true return []fs.DirEntry{}, nil }) + + sys := tv.NewNativeFS(root) _, _ = o.Hooks.ReadDirectory.Default()(sys, root) _, _ = o.Hooks.ReadDirectory.Invoke()(sys, root) @@ -78,12 +90,12 @@ var _ = Describe("Tapable", Ordered, func() { When("QueryStatus hooked", func() { It("🧪 should: invoke hook", func() { - o.Hooks.QueryStatus.Tap(func(_ string) (fs.FileInfo, error) { + o.Hooks.QueryStatus.Tap(func(_ fs.StatFS, _ string) (fs.FileInfo, error) { invoked = true return nil, nil }) - _, _ = o.Hooks.QueryStatus.Default()(root) - _, _ = o.Hooks.QueryStatus.Invoke()(root) + _, _ = o.Hooks.QueryStatus.Default()(emptyFS, root) + _, _ = o.Hooks.QueryStatus.Invoke()(emptyFS, root) Expect(invoked).To(BeTrue(), "QueryStatus hook not invoked") }) diff --git a/traverse-api.go b/traverse-api.go index 8906347..77be0f9 100644 --- a/traverse-api.go +++ b/traverse-api.go @@ -5,6 +5,7 @@ import ( "github.com/snivilised/traverse/core" "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/nfs" "github.com/snivilised/traverse/pref" ) @@ -69,9 +70,14 @@ const ( ResumeStrategyFastward = enums.ResumeStrategyFastward ) +type ( + FileSystems = nfs.FileSystems +) + type Was = pref.Was var ( + // pref If = pref.If WithCPU = pref.WithCPU WithDepth = pref.WithDepth @@ -107,6 +113,9 @@ var ( WithSkipHandler = pref.WithSkipHandler WithSortBehaviour = pref.WithSortBehaviour WithSubPathBehaviour = pref.WithSubPathBehaviour + // nfs + NewNativeFS = nfs.NewNativeFS + NewQueryStatusFS = nfs.NewQueryStatusFS ) type Using = pref.Using @@ -150,6 +159,7 @@ type Using = pref.Using // core: [] // enums: [none] // measure: [] +// nfs: // --- // ============================================================================ //