package toolkit import ( "errors" "fmt" "io/fs" "os" "path/filepath" "testing" ) // Dir is a type which makes it possible to statically assert that a directory // has already been created. type Dir struct { Path string } // MkDir creates a Dir at the given path. // // If the directory already exists, and mayExist is true, then Dir is returned // successfully, otherwise an error is returned. func MkDir(path string, mayExist bool) (Dir, error) { if path == "" { panic("Empty path passed to MkDir") } { parentPath := filepath.Dir(path) parentInfo, err := os.Stat(parentPath) if err != nil { return Dir{}, fmt.Errorf( "checking fs node of parent %q: %w", parentPath, err, ) } else if !parentInfo.IsDir() { return Dir{}, fmt.Errorf( "parent %q is not a directory", parentPath, ) } } info, err := os.Stat(path) if errors.Is(err, fs.ErrNotExist) { // fine } else if err != nil { return Dir{}, fmt.Errorf("checking fs node: %w", err) } else if !info.IsDir() { return Dir{}, fmt.Errorf("exists but is not a directory") } else { if !mayExist { return Dir{}, errors.New("directory already exists") } return Dir{path}, nil } if err := os.Mkdir(path, 0700); err != nil { return Dir{}, fmt.Errorf("creating directory: %w", err) } return Dir{path}, nil } // TempDir returns a Dir based on a temporary directory created via [t.TempDir]. func TempDir(t *testing.T) Dir { return Dir{t.TempDir()} } // MkChildDir is a helper for joining Dir's path to the given name and calling // MkDir with the result. func (d Dir) MkChildDir(name string, mayExist bool) (Dir, error) { childPath := filepath.Join(d.Path, name) d, err := MkDir(childPath, mayExist) if err != nil { return Dir{}, fmt.Errorf( "creating child directory %q: %w", childPath, err, ) } return d, nil } // ChildDirs returns a Dir for every child directory found under this Dir. func (d Dir) ChildDirs() ([]Dir, error) { entries, err := os.ReadDir(d.Path) if errors.Is(err, fs.ErrNotExist) { return nil, nil } else if err != nil { return nil, fmt.Errorf("listing contents: %w", err) } dirs := make([]Dir, 0, len(entries)) for _, entry := range entries { if !entry.IsDir() { continue } dirs = append(dirs, Dir{Path: filepath.Join(d.Path, entry.Name())}) } return dirs, nil } //////////////////////////////////////////////////////////////////////////////// // MkDirHelper is a helper type for creating a set of directories. It will // collect errors as they occur, allowing error handling to be defered. type MkDirHelper struct { errs []error } // Maybe returns the given Dir and true if err == nil. If err != nil then false // is returned, and the error is collected internally such that it will be // returned as part of the Err() output. // // This method is designed to be used in conjunction with a MkDir // function/method, e.g: // // d, ok := h.Maybe(MkDir("some/path", true)) func (m *MkDirHelper) Maybe(d Dir, err error) (Dir, bool) { if err != nil { m.errs = append(m.errs, err) return Dir{}, false } return d, true } // Err returns a join of all errors which have occurred during its lifetime. func (m *MkDirHelper) Err() error { return errors.Join(m.errs...) }