From 4ca1595f1fa6a8ce612b9d1392f726c3797c9fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Sun, 3 Jan 2021 17:09:10 +0100 Subject: [PATCH] Refactor file diff and indexation --- adapter/sqlite/indexer.go | 30 +++++++----- cmd/index.go | 11 +++-- core/{zk => file}/diff.go | 28 +++-------- core/{zk => file}/diff_test.go | 38 +++++++------- core/file/file.go | 29 +++++++++++ core/{zk => file}/fixtures/walk/a.md | 0 core/{zk => file}/fixtures/walk/b.md | 0 .../fixtures/walk/dir1/.ignored.md | 0 .../fixtures/walk/dir1/.ignored/a.md | 0 core/{zk => file}/fixtures/walk/dir1/a.md | 0 core/{zk => file}/fixtures/walk/dir1/b.md | 0 .../{zk => file}/fixtures/walk/dir1/dir1/a.md | 0 .../fixtures/walk/dir1/ignored.txt | 0 core/{zk => file}/fixtures/walk/dir2/a.md | 0 core/{zk/dir.go => file/walk.go} | 34 ++++--------- core/{zk/dir_test.go => file/walk_test.go} | 25 +++++----- core/{zk => note}/index.go | 49 ++++++++++++------- core/zk/id.go | 1 + core/zk/zk.go | 13 +++-- 19 files changed, 141 insertions(+), 117 deletions(-) rename core/{zk => file}/diff.go (76%) rename core/{zk => file}/diff_test.go (87%) create mode 100644 core/file/file.go rename core/{zk => file}/fixtures/walk/a.md (100%) rename core/{zk => file}/fixtures/walk/b.md (100%) rename core/{zk => file}/fixtures/walk/dir1/.ignored.md (100%) rename core/{zk => file}/fixtures/walk/dir1/.ignored/a.md (100%) rename core/{zk => file}/fixtures/walk/dir1/a.md (100%) rename core/{zk => file}/fixtures/walk/dir1/b.md (100%) rename core/{zk => file}/fixtures/walk/dir1/dir1/a.md (100%) rename core/{zk => file}/fixtures/walk/dir1/ignored.txt (100%) rename core/{zk => file}/fixtures/walk/dir2/a.md (100%) rename core/{zk/dir.go => file/walk.go} (50%) rename core/{zk/dir_test.go => file/walk_test.go} (79%) rename core/{zk => note}/index.go (52%) diff --git a/adapter/sqlite/indexer.go b/adapter/sqlite/indexer.go index 2f22d69..11637fd 100644 --- a/adapter/sqlite/indexer.go +++ b/adapter/sqlite/indexer.go @@ -5,11 +5,14 @@ import ( "path/filepath" "time" - "github.com/mickael-menu/zk/core/zk" + "github.com/mickael-menu/zk/core/file" + "github.com/mickael-menu/zk/core/note" "github.com/mickael-menu/zk/util" ) -type Indexer struct { +// NoteIndexer retrieves and stores notes indexation in the SQLite database. +// It implements the Core port note.Indexer. +type NoteIndexer struct { tx *sql.Tx root string logger util.Logger @@ -21,8 +24,11 @@ type Indexer struct { removeStmt *sql.Stmt } -func NewIndexer(tx *sql.Tx, root string, logger util.Logger) (*Indexer, error) { - indexedStmt, err := tx.Prepare("SELECT filename, dir, modified from notes") +func NewNoteIndexer(tx *sql.Tx, root string, logger util.Logger) (*NoteIndexer, error) { + indexedStmt, err := tx.Prepare(` + SELECT filename, dir, modified from notes + ORDER BY dir, filename ASC + `) if err != nil { return nil, err } @@ -52,7 +58,7 @@ func NewIndexer(tx *sql.Tx, root string, logger util.Logger) (*Indexer, error) { return nil, err } - return &Indexer{ + return &NoteIndexer{ tx: tx, root: root, logger: logger, @@ -63,13 +69,13 @@ func NewIndexer(tx *sql.Tx, root string, logger util.Logger) (*Indexer, error) { }, nil } -func (i *Indexer) Indexed() (<-chan zk.FileMetadata, error) { +func (i *NoteIndexer) Indexed() (<-chan file.Metadata, error) { rows, err := i.indexedStmt.Query() if err != nil { return nil, err } - c := make(chan zk.FileMetadata) + c := make(chan file.Metadata) go func() { defer close(c) defer rows.Close() @@ -84,8 +90,8 @@ func (i *Indexer) Indexed() (<-chan zk.FileMetadata, error) { i.logger.Err(err) } - c <- zk.FileMetadata{ - Path: zk.Path{Dir: dir, Filename: filename, Abs: filepath.Join(i.root, dir, filename)}, + c <- file.Metadata{ + Path: file.Path{Dir: dir, Filename: filename, Abs: filepath.Join(i.root, dir, filename)}, Modified: modified, } } @@ -99,7 +105,7 @@ func (i *Indexer) Indexed() (<-chan zk.FileMetadata, error) { return c, nil } -func (i *Indexer) Add(metadata zk.NoteMetadata) error { +func (i *NoteIndexer) Add(metadata note.Metadata) error { _, err := i.addStmt.Exec( metadata.Path.Filename, metadata.Path.Dir, metadata.Title, metadata.Body, metadata.WordCount, metadata.Checksum, @@ -108,7 +114,7 @@ func (i *Indexer) Add(metadata zk.NoteMetadata) error { return err } -func (i *Indexer) Update(metadata zk.NoteMetadata) error { +func (i *NoteIndexer) Update(metadata note.Metadata) error { _, err := i.updateStmt.Exec( metadata.Title, metadata.Body, metadata.WordCount, metadata.Checksum, metadata.Modified, @@ -117,7 +123,7 @@ func (i *Indexer) Update(metadata zk.NoteMetadata) error { return err } -func (i *Indexer) Remove(path zk.Path) error { +func (i *NoteIndexer) Remove(path file.Path) error { _, err := i.updateStmt.Exec(path.Filename, path.Dir) return err } diff --git a/cmd/index.go b/cmd/index.go index 6276d7c..5f7ed34 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/mickael-menu/zk/adapter/sqlite" + "github.com/mickael-menu/zk/core/note" "github.com/mickael-menu/zk/core/zk" ) @@ -11,17 +12,17 @@ type Index struct { } func (cmd *Index) Run(container *Container) error { - z, err := zk.Open(".") + zk, err := zk.Open(".") if err != nil { return err } - dir, err := z.RequireDirAt(cmd.Directory) + dir, err := zk.RequireDirAt(cmd.Directory) if err != nil { return err } - db, err := container.Database(z) + db, err := container.Database(zk) if err != nil { return err } @@ -30,11 +31,11 @@ func (cmd *Index) Run(container *Container) error { if err != nil { return err } - indexer, err := sqlite.NewIndexer(tx, z.Path, container.Logger) + indexer, err := sqlite.NewNoteIndexer(tx, zk.Path, container.Logger) if err != nil { return err } - err = zk.Index(*dir, indexer, container.Logger) + err = note.Index(*dir, indexer, container.Logger) if err != nil { return err } diff --git a/core/zk/diff.go b/core/file/diff.go similarity index 76% rename from core/zk/diff.go rename to core/file/diff.go index 3189d1e..3387da0 100644 --- a/core/zk/diff.go +++ b/core/file/diff.go @@ -1,4 +1,4 @@ -package zk +package file // DiffChange represents a note change made in a slip box directory. type DiffChange struct { @@ -15,14 +15,14 @@ const ( DiffRemoved ) -// Diff compares two sources of FileMetadata and report the note changes, using -// the file checksum and modification date. +// Diff compares two sources of FileMetadata and report the file changes, using +// the file modification date. // // Warning: The FileMetadata have to be sorted by their Path for the diffing to // work properly. -func Diff(source, target <-chan FileMetadata, callback func(DiffChange) error) error { +func Diff(source, target <-chan Metadata, callback func(DiffChange) error) error { var err error - var sourceFile, targetFile FileMetadata + var sourceFile, targetFile Metadata var sourceOpened, targetOpened bool = true, true pair := diffPair{} @@ -50,8 +50,8 @@ func Diff(source, target <-chan FileMetadata, callback func(DiffChange) error) e // diffPair holds the current two files to be diffed. type diffPair struct { - source *FileMetadata - target *FileMetadata + source *Metadata + target *Metadata } // diff compares the source and target files in the current pair. @@ -82,7 +82,7 @@ func (p *diffPair) diff() *DiffChange { p.target = nil default: // Different files, one has been added or removed. - if isAscendingOrder(p.source.Path, p.target.Path) { + if p.source.Path.Less(p.target.Path) { change = &DiffChange{p.source.Path, DiffAdded} p.source = nil } else { @@ -93,15 +93,3 @@ func (p *diffPair) diff() *DiffChange { return change } - -// isAscendingOrder returns true if the source note's path is before the target one. -func isAscendingOrder(source, target Path) bool { - switch { - case source.Dir < target.Dir: - return true - case source.Dir > target.Dir: - return false - default: - return source.Filename < target.Filename - } -} diff --git a/core/zk/diff_test.go b/core/file/diff_test.go similarity index 87% rename from core/zk/diff_test.go rename to core/file/diff_test.go index f5b2db0..3a1fcd1 100644 --- a/core/zk/diff_test.go +++ b/core/file/diff_test.go @@ -1,4 +1,4 @@ -package zk +package file import ( "errors" @@ -14,13 +14,13 @@ var date3 = time.Date(2014, 12, 10, 3, 34, 58, 651387237, time.UTC) var date4 = time.Date(2016, 13, 11, 4, 34, 58, 651387237, time.UTC) func TestDiffEmpty(t *testing.T) { - source := []FileMetadata{} - target := []FileMetadata{} + source := []Metadata{} + target := []Metadata{} test(t, source, target, []DiffChange{}) } func TestNoDiff(t *testing.T) { - files := []FileMetadata{ + files := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -39,7 +39,7 @@ func TestNoDiff(t *testing.T) { } func TestDiff(t *testing.T) { - source := []FileMetadata{ + source := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -54,7 +54,7 @@ func TestDiff(t *testing.T) { }, } - target := []FileMetadata{ + target := []Metadata{ { // Date changed Path: Path{Dir: "a", Filename: "1"}, @@ -90,7 +90,7 @@ func TestDiff(t *testing.T) { } func TestDiffWithMoreInSource(t *testing.T) { - source := []FileMetadata{ + source := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -101,7 +101,7 @@ func TestDiffWithMoreInSource(t *testing.T) { }, } - target := []FileMetadata{ + target := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -117,14 +117,14 @@ func TestDiffWithMoreInSource(t *testing.T) { } func TestDiffWithMoreInTarget(t *testing.T) { - source := []FileMetadata{ + source := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, }, } - target := []FileMetadata{ + target := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -144,9 +144,9 @@ func TestDiffWithMoreInTarget(t *testing.T) { } func TestDiffEmptySource(t *testing.T) { - source := []FileMetadata{} + source := []Metadata{} - target := []FileMetadata{ + target := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -170,7 +170,7 @@ func TestDiffEmptySource(t *testing.T) { } func TestDiffEmptyTarget(t *testing.T) { - source := []FileMetadata{ + source := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -181,7 +181,7 @@ func TestDiffEmptyTarget(t *testing.T) { }, } - target := []FileMetadata{} + target := []Metadata{} test(t, source, target, []DiffChange{ { @@ -196,7 +196,7 @@ func TestDiffEmptyTarget(t *testing.T) { } func TestDiffCancellation(t *testing.T) { - source := []FileMetadata{ + source := []Metadata{ { Path: Path{Dir: "a", Filename: "1"}, Modified: date1, @@ -207,7 +207,7 @@ func TestDiffCancellation(t *testing.T) { }, } - target := []FileMetadata{} + target := []Metadata{} received := make([]DiffChange, 0) err := Diff(toChannel(source), toChannel(target), func(change DiffChange) error { @@ -229,7 +229,7 @@ func TestDiffCancellation(t *testing.T) { assert.Err(t, err, "cancelled") } -func test(t *testing.T, source, target []FileMetadata, expected []DiffChange) { +func test(t *testing.T, source, target []Metadata, expected []DiffChange) { received := make([]DiffChange, 0) err := Diff(toChannel(source), toChannel(target), func(change DiffChange) error { received = append(received, change) @@ -239,8 +239,8 @@ func test(t *testing.T, source, target []FileMetadata, expected []DiffChange) { assert.Equal(t, received, expected) } -func toChannel(fm []FileMetadata) <-chan FileMetadata { - c := make(chan FileMetadata) +func toChannel(fm []Metadata) <-chan Metadata { + c := make(chan Metadata) go func() { for _, m := range fm { c <- m diff --git a/core/file/file.go b/core/file/file.go new file mode 100644 index 0000000..d13e49f --- /dev/null +++ b/core/file/file.go @@ -0,0 +1,29 @@ +package file + +import "time" + +// Metadata holds information about a slip box file. +type Metadata struct { + Path Path + Modified time.Time +} + +// Path holds a file path relative to a slip box. +type Path struct { + Dir string + Filename string + Abs string +} + +// Less returns whether ther receiver path is located before the given one, +// lexicographically. +func (p Path) Less(other Path) bool { + switch { + case p.Dir < other.Dir: + return true + case p.Dir > other.Dir: + return false + default: + return p.Filename < other.Filename + } +} diff --git a/core/zk/fixtures/walk/a.md b/core/file/fixtures/walk/a.md similarity index 100% rename from core/zk/fixtures/walk/a.md rename to core/file/fixtures/walk/a.md diff --git a/core/zk/fixtures/walk/b.md b/core/file/fixtures/walk/b.md similarity index 100% rename from core/zk/fixtures/walk/b.md rename to core/file/fixtures/walk/b.md diff --git a/core/zk/fixtures/walk/dir1/.ignored.md b/core/file/fixtures/walk/dir1/.ignored.md similarity index 100% rename from core/zk/fixtures/walk/dir1/.ignored.md rename to core/file/fixtures/walk/dir1/.ignored.md diff --git a/core/zk/fixtures/walk/dir1/.ignored/a.md b/core/file/fixtures/walk/dir1/.ignored/a.md similarity index 100% rename from core/zk/fixtures/walk/dir1/.ignored/a.md rename to core/file/fixtures/walk/dir1/.ignored/a.md diff --git a/core/zk/fixtures/walk/dir1/a.md b/core/file/fixtures/walk/dir1/a.md similarity index 100% rename from core/zk/fixtures/walk/dir1/a.md rename to core/file/fixtures/walk/dir1/a.md diff --git a/core/zk/fixtures/walk/dir1/b.md b/core/file/fixtures/walk/dir1/b.md similarity index 100% rename from core/zk/fixtures/walk/dir1/b.md rename to core/file/fixtures/walk/dir1/b.md diff --git a/core/zk/fixtures/walk/dir1/dir1/a.md b/core/file/fixtures/walk/dir1/dir1/a.md similarity index 100% rename from core/zk/fixtures/walk/dir1/dir1/a.md rename to core/file/fixtures/walk/dir1/dir1/a.md diff --git a/core/zk/fixtures/walk/dir1/ignored.txt b/core/file/fixtures/walk/dir1/ignored.txt similarity index 100% rename from core/zk/fixtures/walk/dir1/ignored.txt rename to core/file/fixtures/walk/dir1/ignored.txt diff --git a/core/zk/fixtures/walk/dir2/a.md b/core/file/fixtures/walk/dir2/a.md similarity index 100% rename from core/zk/fixtures/walk/dir2/a.md rename to core/file/fixtures/walk/dir2/a.md diff --git a/core/zk/dir.go b/core/file/walk.go similarity index 50% rename from core/zk/dir.go rename to core/file/walk.go index 0d639f6..94738aa 100644 --- a/core/zk/dir.go +++ b/core/file/walk.go @@ -1,37 +1,21 @@ -package zk +package file import ( "os" "path/filepath" "strings" - "time" + "github.com/mickael-menu/zk/core/zk" "github.com/mickael-menu/zk/util" ) -// Dir represents a directory inside a slip box. -type Dir struct { - // Name of the directory, which is the path relative to the slip box's root. - Name string - // Absolute path to the directory. - Path string - // User configuration for this directory. - Config DirConfig -} - -// FileMetadata holds information about a note file. -type FileMetadata struct { - Path Path - Modified time.Time -} - -// Walk emits the metadata of each note stored in the directory. -func (d Dir) Walk(logger util.Logger) <-chan FileMetadata { - c := make(chan FileMetadata, 50) +// Walk emits the metadata of each file stored in the directory. +func Walk(dir zk.Dir, logger util.Logger) <-chan Metadata { + c := make(chan Metadata, 50) go func() { defer close(c) - err := filepath.Walk(d.Path, func(abs string, info os.FileInfo, err error) error { + err := filepath.Walk(dir.Path, func(abs string, info os.FileInfo, err error) error { if err != nil { return err } @@ -50,7 +34,7 @@ func (d Dir) Walk(logger util.Logger) <-chan FileMetadata { return nil } - path, err := filepath.Rel(d.Path, abs) + path, err := filepath.Rel(dir.Path, abs) if err != nil { logger.Println(err) return nil @@ -61,9 +45,9 @@ func (d Dir) Walk(logger util.Logger) <-chan FileMetadata { curDir = "" } - c <- FileMetadata{ + c <- Metadata{ Path: Path{ - Dir: filepath.Join(d.Name, curDir), + Dir: filepath.Join(dir.Name, curDir), Filename: filename, Abs: abs, }, diff --git a/core/zk/dir_test.go b/core/file/walk_test.go similarity index 79% rename from core/zk/dir_test.go rename to core/file/walk_test.go index 0adaf27..9be0b05 100644 --- a/core/zk/dir_test.go +++ b/core/file/walk_test.go @@ -1,10 +1,11 @@ -package zk +package file import ( "path/filepath" "testing" "time" + "github.com/mickael-menu/zk/core/zk" "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/assert" "github.com/mickael-menu/zk/util/fixtures" @@ -13,9 +14,9 @@ import ( var root = fixtures.Path("walk") func TestWalkRootDir(t *testing.T) { - dir := Dir{Name: "", Path: root} - res := toSlice(dir.Walk(&util.NullLogger)) - assert.Equal(t, res, []FileMetadata{ + dir := zk.Dir{Name: "", Path: root} + res := toSlice(Walk(dir, &util.NullLogger)) + assert.Equal(t, res, []Metadata{ { Path: Path{Dir: "", Filename: "a.md", Abs: filepath.Join(root, "a.md")}, Modified: date("2021-01-03T11:30:26.069257899+01:00"), @@ -44,9 +45,9 @@ func TestWalkRootDir(t *testing.T) { } func TestWalkSubDir(t *testing.T) { - dir := Dir{Name: "dir1", Path: filepath.Join(root, "dir1")} - res := toSlice(dir.Walk(&util.NullLogger)) - assert.Equal(t, res, []FileMetadata{ + dir := zk.Dir{Name: "dir1", Path: filepath.Join(root, "dir1")} + res := toSlice(Walk(dir, &util.NullLogger)) + assert.Equal(t, res, []Metadata{ { Path: Path{Dir: "dir1", Filename: "a.md", Abs: filepath.Join(root, "dir1/a.md")}, Modified: date("2021-01-03T11:31:18.961628888+01:00"), @@ -63,9 +64,9 @@ func TestWalkSubDir(t *testing.T) { } func TestWalkSubSubDir(t *testing.T) { - dir := Dir{Name: "dir1/dir1", Path: filepath.Join(root, "dir1/dir1")} - res := toSlice(dir.Walk(&util.NullLogger)) - assert.Equal(t, res, []FileMetadata{ + dir := zk.Dir{Name: "dir1/dir1", Path: filepath.Join(root, "dir1/dir1")} + res := toSlice(Walk(dir, &util.NullLogger)) + assert.Equal(t, res, []Metadata{ { Path: Path{Dir: "dir1/dir1", Filename: "a.md", Abs: filepath.Join(root, "dir1/dir1/a.md")}, Modified: date("2021-01-03T11:31:27.900472856+01:00"), @@ -78,8 +79,8 @@ func date(s string) time.Time { return date } -func toSlice(c <-chan FileMetadata) []FileMetadata { - s := make([]FileMetadata, 0) +func toSlice(c <-chan Metadata) []Metadata { + s := make([]Metadata, 0) for fm := range c { s = append(s, fm) } diff --git a/core/zk/index.go b/core/note/index.go similarity index 52% rename from core/zk/index.go rename to core/note/index.go index b10dc13..0833a06 100644 --- a/core/zk/index.go +++ b/core/note/index.go @@ -1,4 +1,4 @@ -package zk +package note import ( "crypto/sha256" @@ -7,14 +7,16 @@ import ( "strings" "time" + "github.com/mickael-menu/zk/core/file" + "github.com/mickael-menu/zk/core/zk" "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/errors" "gopkg.in/djherbis/times.v1" ) -// NoteMetadata holds information about a particular note. -type NoteMetadata struct { - Path Path +// Metadata holds information about a particular note. +type Metadata struct { + Path file.Path Title string Body string WordCount int @@ -23,48 +25,57 @@ type NoteMetadata struct { Checksum string } +// Indexer persists the notes index. type Indexer interface { - Indexed() (<-chan FileMetadata, error) - Add(metadata NoteMetadata) error - Update(metadata NoteMetadata) error - Remove(path Path) error + // Indexed returns the list of indexed note file metadata. + Indexed() (<-chan file.Metadata, error) + // Add indexes a new note from its metadata. + Add(metadata Metadata) error + // Update updates the metadata of an already indexed note. + Update(metadata Metadata) error + // Remove deletes a note from the index. + Remove(path file.Path) error } // Index indexes the content of the notes in the given directory. -func Index(dir Dir, indexer Indexer, logger util.Logger) error { +func Index(dir zk.Dir, indexer Indexer, logger util.Logger) error { wrap := errors.Wrapper("indexation failed") - source := dir.Walk(logger) + source := file.Walk(dir, logger) target, err := indexer.Indexed() if err != nil { return wrap(err) } - return Diff(source, target, func(change DiffChange) error { + err = file.Diff(source, target, func(change file.DiffChange) error { switch change.Kind { - case DiffAdded: - metadata, err := noteMetadata(change.Path) + case file.DiffAdded: + metadata, err := metadata(change.Path) if err == nil { err = indexer.Add(metadata) } logger.Err(err) - case DiffModified: - metadata, err := noteMetadata(change.Path) + case file.DiffModified: + metadata, err := metadata(change.Path) if err == nil { err = indexer.Update(metadata) } logger.Err(err) - case DiffRemoved: - indexer.Remove(change.Path) + case file.DiffRemoved: + err := indexer.Remove(change.Path) + logger.Err(err) } return nil }) + + return wrap(err) } -func noteMetadata(path Path) (NoteMetadata, error) { - metadata := NoteMetadata{ +// metadata retrieves note metadata for the given file. +func metadata(path file.Path) (Metadata, error) { + metadata := Metadata{ Path: path, } diff --git a/core/zk/id.go b/core/zk/id.go index 720d8be..77f5ed6 100644 --- a/core/zk/id.go +++ b/core/zk/id.go @@ -7,6 +7,7 @@ type IDOptions struct { Case Case } +// Charset is a set of characters. type Charset []rune var ( diff --git a/core/zk/zk.go b/core/zk/zk.go index 87f51f1..a333035 100644 --- a/core/zk/zk.go +++ b/core/zk/zk.go @@ -24,11 +24,14 @@ type Zk struct { Config Config } -// Path holds a file path relative to a slip box. -type Path struct { - Dir string - Filename string - Abs string +// Dir represents a directory inside a slip box. +type Dir struct { + // Name of the directory, which is the path relative to the slip box's root. + Name string + // Absolute path to the directory. + Path string + // User configuration for this directory. + Config DirConfig } // Open locates a slip box at the given path and parses its configuration.