From 539db7f3ad01cdeac24c75d442cdc7748cd5eb0e Mon Sep 17 00:00:00 2001 From: g026r Date: Fri, 27 Sep 2024 21:50:58 -0400 Subject: [PATCH] Option to overwrite original files when saving (#17) --- README.md | 13 ++-- cmd/playfix/main.go | 14 +++- pkg/io/io.go | 59 +++------------ pkg/ui/menus.go | 74 ++++++++++--------- pkg/ui/model.go | 170 ++++++++++++++++++++++++++++--------------- pkg/ui/model_test.go | 12 +-- pkg/util/func.go | 34 +++++++++ 7 files changed, 224 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index a184c57..2fb60e2 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,14 @@ experimental software that is doing something Analogue was not expecting users t ## How to Install -Download the appropriate version for your operating system from [the latest release](https://github.com/g026r/pocket-library-toolkit/releases/latest) & extract the archive. I recommend placing it in the root of your Pocket's SD card. +Download the appropriate version for your operating system +from [the latest release](https://github.com/g026r/pocket-library-toolkit/releases/latest) & extract the archive. I +recommend placing it in the root of your Pocket's SD card. Run the application & select the actions you desire. A basic [user guide](docs/userguide.md) is available. -Once complete, and then copy the files the tool generates over to the correct locations under your SD card's System directory _**making backups of any originals before replacing them**_. +Once complete, then copy the files the tool generates over to the correct locations under your SD card's System +directory _**making backups of any originals before replacing them**_. ## But Why? @@ -21,8 +24,8 @@ Because I can get remarkably anal about these things. First off: 95% of Pocket users won't need or even want this. -This software is for the users who are annoyed that their library shows `Famicom Mini 01 - Super Mario Bros.` but also -`Famicom Mini 22: Nazo no Murasame Jou`, that it's `The Lion King` but `NewZealand Story, The`. +This software is for the users who are annoyed that their library shows _Famicom Mini 01 - Super Mario Bros._ but also +_Famicom Mini 22: Nazo no Murasame Jou_, that it's _The Lion King_ but _NewZealand Story, The_. It's for those users who have one of the small number of carts that the Pocket misidentifies & who'd rather it appeared in their library under the correct name. @@ -33,7 +36,7 @@ manually editing the binary file themselves. ## Limitations The library info screen for a given cart is stored in the Pocket's internal memory. Even if your library -now shows "Sagaia" instead of "Mani 4 in 1 - Taito", clicking into it or loading the cart will still show you the +now shows "_Sagaia_" instead of "_Mani 4 in 1 - Taito_", clicking into it or loading the cart will still show you the original info. Additionally, if you have two different entries with the same cart signature, it's likely that only the playtime for the diff --git a/cmd/playfix/main.go b/cmd/playfix/main.go index dc6a4a6..d9f6735 100644 --- a/cmd/playfix/main.go +++ b/cmd/playfix/main.go @@ -2,8 +2,10 @@ package main import ( "encoding/binary" + "fmt" "log" "os" + "path/filepath" "github.com/g026r/pocket-toolkit/pkg/io" ) @@ -11,11 +13,17 @@ import ( // Simple application to fix played times & nothing else. func main() { - entries, err := io.LoadEntries(os.DirFS("./")) + ex, err := os.Executable() if err != nil { log.Fatal(err) } - p, err := io.LoadPlaytimes(os.DirFS("./")) + root := filepath.Dir(ex) + + entries, err := io.LoadEntries(os.DirFS(root)) + if err != nil { + log.Fatal(err) + } + p, err := io.LoadPlaytimes(os.DirFS(root)) if err != nil { log.Fatal(err) } @@ -28,7 +36,7 @@ func main() { defer func() { _ = out.Close() if complete { // Overwrite the original with the temp file if successful; delete it if not. - err = os.Rename(out.Name(), "System/Played Games/playtimes.bin") + err = os.Rename(out.Name(), fmt.Sprintf("%s/System/Played Games/playtimes.bin", root)) } else { err = os.Remove(out.Name()) } diff --git a/pkg/io/io.go b/pkg/io/io.go index 58306f2..1b46459 100644 --- a/pkg/io/io.go +++ b/pkg/io/io.go @@ -35,6 +35,8 @@ type Config struct { AdvancedEditing bool `json:"advanced_editing"` ShowAdd bool `json:"show_add"` GenerateNew bool `json:"generate_new"` + SaveUnmodified bool `json:"save_unmodified"` + Overwrite bool `json:"overwrite"` } type jsonEntry struct { @@ -42,7 +44,7 @@ type jsonEntry struct { Name string `json:"name"` Crc32 string `json:"crc"` Sig string `json:"signature"` - Magic string `json:"magic"` // TODO: Work out all possible mappings for this + Magic string `json:"magic"` // TODO: Work out all possible mappings for this? } func (j jsonEntry) Entry() models.Entry { @@ -249,8 +251,11 @@ func LoadConfig() (Config, error) { AdvancedEditing: false, ShowAdd: false, GenerateNew: true, + SaveUnmodified: false, + Overwrite: false, } - //// FIXME: Use the program's dir rather than the cwd + // FIXME: When compiling, use the program's dir rather than the cwd + // FIXME: When testing, use the cwd & remember to comment out the filepath.Dir call //dir, err := os.Getwd() dir, err := os.Executable() if err != nil { @@ -302,23 +307,7 @@ func LoadInternal() (map[models.System][]models.Entry, error) { return library, nil } -func SaveLibrary(e []models.Entry, t map[uint32]models.PlayTime, tick chan any) error { - wd, err := os.Getwd() - if err != nil { - return err - } - l, err := os.Create(fmt.Sprintf("%s/pocket-toolkit/list.bin", wd)) - if err != nil { - return err - } - defer l.Close() - - p, err := os.Create(fmt.Sprintf("%s/pocket-toolkit/playtimes.bin", wd)) - if err != nil { - return err - } - defer p.Close() - +func SaveLibrary(l io.Writer, e []models.Entry, p io.Writer, t map[uint32]models.PlayTime, tick chan any) error { // Prep list.bin if err := binary.Write(l, binary.BigEndian, ListHeader); err != nil { return err @@ -377,32 +366,7 @@ func SaveLibrary(e []models.Entry, t map[uint32]models.PlayTime, tick chan any) return nil } -func SaveThumbs(t map[models.System]models.Thumbnails, tick chan any) error { - wd, err := os.Getwd() - if err != nil { - return err - } - - for sys, thumbs := range t { - if !thumbs.Modified { - continue // Not changed. For speed reasons, don't save. - } - - f, err := os.Create(fmt.Sprintf("%s/pocket-toolkit/%s_thumbs.bin", wd, strings.ToLower(sys.String()))) - if err != nil { - return err - } - - err = writeThumbsFile(f, thumbs.Images, tick) - _ = f.Close() // Close explicitly rather than defer as defer in a loop is not best practice - if err != nil { - return err - } - } - return nil -} - -func writeThumbsFile(t io.Writer, img []models.Image, tick chan any) error { +func SaveThumbsFile(t io.Writer, img []models.Image, tick chan any) error { if err := binary.Write(t, binary.LittleEndian, ThumbnailHeader); err != nil { return err } @@ -444,8 +408,9 @@ func SaveConfig(config Config) error { if err != nil { return err } - //// FIXME: Use the program's dir rather than the cwd - //dir, err := os.Getwd() + // FIXME: When compiling, use the program's dir rather than the cwd + // FIXME: When testing, use the cwd & remember to comment out the filepath.Dir call + // dir, err := os.Getwd() dir, err := os.Executable() if err != nil { return err diff --git a/pkg/ui/menus.go b/pkg/ui/menus.go index 3c2809d..22c41b6 100644 --- a/pkg/ui/menus.go +++ b/pkg/ui/menus.go @@ -25,20 +25,22 @@ const ( about save quit - add - edit - rm - fix + libAdd + libEdit + libRm + libFix back - missing - single - genlib - all - prune - showAdd - advEdit - rmThumbs - genNew + tmMissing + tmSingle + tmGenlib + tmAll + tmPrune + cfgShowAdd + cfgAdvEdit + cfgRmThumbs + cfgGenNew + cfgUnmodified + cfgOverwrite ) var ( @@ -73,23 +75,25 @@ var ( menuItem{"Save & Quit", save}, menuItem{"Quit", quit}} libraryOptions = []list.Item{ - menuItem{"Add entry", add}, - menuItem{"Edit entry", edit}, - menuItem{"Remove entry", rm}, - menuItem{"Fix played times", fix}, + menuItem{"Add entry", libAdd}, + menuItem{"Edit entry", libEdit}, + menuItem{"Remove entry", libRm}, + menuItem{"Fix played times", libFix}, menuItem{"Back", back}} thumbOptions = []list.Item{ - menuItem{"Generate missing thumbnails", missing}, - menuItem{"Regenerate single game", single}, - menuItem{"Regenerate full library", genlib}, - menuItem{"Prune orphaned thumbnails", prune}, - menuItem{"Generate complete system thumbnails", all}, + menuItem{"Generate tmMissing thumbnails", tmMissing}, + menuItem{"Regenerate tmSingle game", tmSingle}, + menuItem{"Regenerate full library", tmGenlib}, + menuItem{"Prune orphaned thumbnails", tmPrune}, + menuItem{"Generate complete system thumbnails", tmAll}, menuItem{"Back", back}} configOptions = []list.Item{ - menuItem{"Remove thumbnail when removing game", rmThumbs}, - menuItem{"Generate new thumbnail when editing game", genNew}, - //menuItem{"Show advanced library editing fields " + italic.Render("(Experimental)"), advEdit}, - //menuItem{"Show 'Add to Library' " + italic.Render("(Experimental)"), showAdd}, + menuItem{"Remove thumbnail when removing game", cfgRmThumbs}, + menuItem{"Generate new thumbnail when editing game", cfgGenNew}, + menuItem{"Overwrite original files on save", cfgOverwrite}, + menuItem{"Always save _thumbs.bin files, even if unmodified", cfgUnmodified}, + //menuItem{"Show advanced library editing fields " + italic.Render("(Experimental)"), cfgAdvEdit}, + //menuItem{"Show 'Add to Library' " + italic.Render("(Experimental)"), cfgShowAdd}, menuItem{"Back", back}} // esc consists of the items to be performed if esc is typed @@ -211,7 +215,7 @@ var ( } ) -// defaulAction is a default action for sub menus allowing numeric navigation. +// defaultAction is a default action for sub menus allowing numeric navigation. // It's not easily doable for game list menus as there may be too many items to handle key-presses without storing the previous press & waiting to process it. func defaultAction(scr screen, menu *list.Model, m *Model, msg tea.Msg) (*Model, tea.Cmd) { if k, ok := msg.(tea.KeyMsg); ok { @@ -303,14 +307,18 @@ func (d configDelegate) Render(w goio.Writer, m list.Model, index int, listItem str = fmt.Sprintf("%d. [%%s] %s", index+1, i) var b bool switch i.key { - case advEdit: - b = d.AdvancedEditing - case showAdd: - b = d.ShowAdd - case rmThumbs: + case cfgRmThumbs: b = d.RemoveImages - case genNew: + case cfgGenNew: b = d.GenerateNew + case cfgUnmodified: + b = d.SaveUnmodified + case cfgOverwrite: + b = d.Overwrite + case cfgAdvEdit: + b = d.AdvancedEditing + case cfgShowAdd: + b = d.ShowAdd default: // If we don't know what this value is, return return diff --git a/pkg/ui/model.go b/pkg/ui/model.go index 7b83b27..7f21618 100644 --- a/pkg/ui/model.go +++ b/pkg/ui/model.go @@ -6,8 +6,8 @@ import ( "encoding/hex" "fmt" "io/fs" + "log" "os" - "path/filepath" "slices" "strings" "time" @@ -131,14 +131,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd case tea.WindowSizeMsg: m.progress.Width = msg.Width - 8 - m.mainMenu.SetHeight(msg.Height) - m.mainMenu.SetWidth(msg.Width) - m.subMenu.SetHeight(msg.Height) - m.subMenu.SetWidth(msg.Width) - m.configMenu.SetHeight(msg.Height) - m.configMenu.SetWidth(msg.Width) - m.gameList.SetHeight(msg.Height) - m.gameList.SetWidth(msg.Width) + m.mainMenu.SetHeight(msg.Height - 1) + m.mainMenu.SetWidth(msg.Width - 1) + m.subMenu.SetHeight(msg.Height - 1) + m.subMenu.SetWidth(msg.Width - 1) + m.configMenu.SetHeight(msg.Height - 1) + m.configMenu.SetWidth(msg.Width - 1) + m.gameList.SetHeight(msg.Height - 1) + m.gameList.SetWidth(msg.Width - 1) return m, nil case initDoneMsg: m.initialized = true @@ -212,30 +212,9 @@ func aboutView() string { // initSystem loads all our data from disk func (m *Model) initSystem() tea.Msg { - var d string - var err error - - switch len(os.Args) { - case 1: - if d, err = os.Executable(); err != nil { - return errMsg{err, true} - } - d = filepath.Dir(d) - case 2: - d = os.Args[1] - default: - } - - d, err = filepath.Abs(d) - if err != nil { - return errMsg{err, true} - } - - fi, err := os.Stat(d) + d, err := util.GetRoot() if err != nil { return errMsg{err, true} - } else if !fi.IsDir() { - return errMsg{fmt.Errorf("%s is not a directory", d), true} } m.rootDir = os.DirFS(d) @@ -280,14 +259,83 @@ func (m *Model) initSystem() tea.Msg { // save is the opposite of init: save our data to disk func (m *Model) save() tea.Msg { - wd, err := os.Getwd() + success := false + tmpList, err := os.CreateTemp("", "list.bin_*") if err != nil { - return err + return errMsg{err, true} } - err = os.Mkdir(fmt.Sprintf("%s/pocket-toolkit", wd), os.ModePerm) - if err != nil && !os.IsExist(err) { + tmpPlaytimes, err := os.CreateTemp("", "playtimes.bin_*") + if err != nil { return errMsg{err, true} } + tmpThumbs := make(map[models.System]*os.File) + for k, v := range m.thumbnails { + if v.Modified || m.SaveUnmodified { + tmpThumbs[k], err = os.CreateTemp("", fmt.Sprintf("%s_thumbs.bin_*", strings.ToLower(k.String()))) + if err != nil { + return errMsg{err, true} + } + } + } + defer func() { + // An absolute mess of a function that: + // 1. Closes all the file handles + // 2. Creates the output directories if we're not overwriting + // 3. Moves the temporary files over to the correct spot if successful, or + // 4. Deletes them if we weren't + // TODO: Clean this up to make it all more manageable + + // Clean everything up + _ = tmpList.Close() + _ = tmpPlaytimes.Close() + for _, v := range tmpThumbs { + _ = v.Close() + } + if success { + var root string + var err error + if m.Overwrite { + root, err = util.GetRoot() + if err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + } else { + // If we're not overwriting in place, get the working dir & create all our directories if they don't exist + wd, err := os.Getwd() + if err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + root = fmt.Sprintf("%s/pocket-toolkit", wd) + _ = os.Mkdir(root, os.ModePerm) + _ = os.Mkdir(fmt.Sprintf("%s/System", root), os.ModePerm) + if len(tmpThumbs) > 0 { + _ = os.Mkdir(fmt.Sprintf("%s/System/Library", root), os.ModePerm) + _ = os.Mkdir(fmt.Sprintf("%s/System/Library/Images", root), os.ModePerm) + } + if err := os.Mkdir(fmt.Sprintf("%s/System/Played Games", root), os.ModePerm); err != nil && !os.IsExist(err) { + log.Fatal(errorStyle.Render(err.Error())) // Only going to check this final one for errors on the basis of "if it failed, the others did as well" + } + } + if err := os.Rename(tmpList.Name(), fmt.Sprintf("%s/System/Played Games/list.bin", root)); err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + if err := os.Rename(tmpPlaytimes.Name(), fmt.Sprintf("%s/System/Played Games/playtimes.bin", root)); err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + for k, v := range tmpThumbs { + if err := os.Rename(v.Name(), fmt.Sprintf("%s/System/Library/Images/%s_thumbs.bin", root, strings.ToLower(k.String()))); err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + } + } else { + // Remove all the files as we weren't successful + _ = os.Remove(tmpList.Name()) + _ = os.Remove(tmpPlaytimes.Name()) + for _, v := range tmpThumbs { + _ = os.Remove(v.Name()) + } + } + }() ctr := 0.0 tick := make(chan any) @@ -301,13 +349,15 @@ func (m *Model) save() tea.Msg { go func() { // Run these in a goroutine to avoid having to pass around the pointer to the progress value as that would require knowing the total as well defer close(tick) - if err := io.SaveLibrary(m.entries, m.playTimes, tick); err != nil { + if err := io.SaveLibrary(tmpList, m.entries, tmpPlaytimes, m.playTimes, tick); err != nil { tick <- err return } - if err := io.SaveThumbs(m.thumbnails, tick); err != nil { - tick <- err - return + for k, v := range tmpThumbs { + if err := io.SaveThumbsFile(v, m.thumbnails[k].Images, tick); err != nil { + tick <- err + return + } } if err := io.SaveConfig(*m.Config); err != nil { tick <- err @@ -326,6 +376,7 @@ func (m *Model) save() tea.Msg { } } + success = true return tea.QuitMsg{} } @@ -475,7 +526,7 @@ func (m *Model) regenLib() tea.Msg { return updateMsg{} } -// genSingle generates a single thumbnail entry & then either updates or inserts it into the list of thumbnails +// genSingle generates a tmSingle thumbnail entry & then either updates or inserts it into the list of thumbnails func (m *Model) genSingle(e models.Entry) tea.Cmd { return func() tea.Msg { m.percent = 0.0 @@ -752,7 +803,6 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { case about: m.Push(AboutScreen) m.anyKey = true - // TODO: Add a basic about screen case quit: return m, tea.Quit case save: @@ -761,7 +811,7 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { return m, tea.Batch(m.save, tickCmd()) case back: return pop(m, nil) - case add: + case libAdd: m.focusedInput = 0 for i := range len(m.gameInput) { m.gameInput[i].Style(itemStyle) @@ -775,41 +825,41 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { m.gameInput[m.focusedInput].Style(focusedStyle) m.Push(AddScreen) return m, m.gameInput[m.focusedInput].Focus() - case edit: + case libEdit: m.gameList = generateGameList(m.gameList, m.entries, "Main > Library > Edit Game", m.mainMenu.Width(), m.mainMenu.Height()) m.Push(EditList) - case rm: + case libRm: m.gameList = generateGameList(m.gameList, m.entries, "Main > Library > Remove Game", m.mainMenu.Width(), m.mainMenu.Height()) m.Push(RemoveList) - case fix: + case libFix: m.Push(Waiting) m.wait = "Fixing played times" m.percent = 0.0 return m, tea.Batch(m.playfix, tickCmd()) - case missing: + case tmMissing: m.Push(Waiting) m.percent = 0.0 - m.wait = "Generating missing thumbnails for library" + m.wait = "Generating tmMissing thumbnails for library" return m, tea.Batch(m.genMissing, tickCmd()) - case single: + case tmSingle: m.gameList = generateGameList(m.gameList, m.entries, "Main > Library > Generate Thumbnail", m.mainMenu.Width(), m.mainMenu.Height()) m.Push(GenerateList) - case genlib: + case tmGenlib: m.Push(Waiting) m.percent = 0.0 - m.wait = "Regenerating all thumbnails for library" + m.wait = "Regenerating tmAll thumbnails for library" return m, tea.Batch(m.regenLib, tickCmd()) - case prune: + case tmPrune: m.Push(Waiting) m.percent = 0.0 m.wait = "Removing orphaned thumbs.bin entries" return m, tea.Batch(m.prune, tickCmd()) - case all: + case tmAll: m.Push(Waiting) m.percent = 0.0 - m.wait = "Generating thumbnails for all games in the Images folder. This may take a while." + m.wait = "Generating thumbnails for tmAll games in the Images folder. This may take a while." return m, tea.Batch(m.genFull, tickCmd()) - case showAdd, advEdit, rmThumbs, genNew: + case cfgShowAdd, cfgAdvEdit, cfgRmThumbs, cfgGenNew, cfgOverwrite, cfgUnmodified: return m.configChange(key) } @@ -819,14 +869,18 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { // configMenu handles item selection on the settings menu func (m *Model) configChange(key menuKey) (*Model, tea.Cmd) { switch key { - case showAdd: + case cfgShowAdd: m.ShowAdd = !m.ShowAdd - case rmThumbs: + case cfgRmThumbs: m.RemoveImages = !m.RemoveImages - case advEdit: + case cfgAdvEdit: m.AdvancedEditing = !m.AdvancedEditing - case genNew: + case cfgGenNew: m.GenerateNew = !m.GenerateNew + case cfgOverwrite: + m.Overwrite = !m.Overwrite + case cfgUnmodified: + m.SaveUnmodified = !m.SaveUnmodified } return m, nil diff --git a/pkg/ui/model_test.go b/pkg/ui/model_test.go index a1eca9f..2670e05 100644 --- a/pkg/ui/model_test.go +++ b/pkg/ui/model_test.go @@ -101,17 +101,17 @@ func TestModel_configChange(t *testing.T) { sut := Model{Config: &config} // test all false -> true - m, _ := sut.configChange(showAdd) + m, _ := sut.configChange(cfgShowAdd) if !m.ShowAdd || m.AdvancedEditing || m.RemoveImages { t.Errorf("Expected ShowAdd to be true: %v", *m.Config) } *m.Config = io.Config{} - m, _ = sut.configChange(rmThumbs) + m, _ = sut.configChange(cfgRmThumbs) if !m.RemoveImages || m.AdvancedEditing || m.ShowAdd { t.Errorf("Expected RemoveImages to be true: %v", *m.Config) } *m.Config = io.Config{} - m, _ = sut.configChange(advEdit) + m, _ = sut.configChange(cfgAdvEdit) if !m.AdvancedEditing || m.ShowAdd || m.RemoveImages { t.Errorf("Expected AdvancedEditing to be true: %v", *m.Config) } @@ -123,17 +123,17 @@ func TestModel_configChange(t *testing.T) { } // test true -> false *sut.Config = allTrue - m, _ = sut.configChange(showAdd) + m, _ = sut.configChange(cfgShowAdd) if m.ShowAdd || !m.AdvancedEditing || !m.RemoveImages { t.Errorf("Expected ShowAdd to be false: %v", *m.Config) } *sut.Config = allTrue - m, _ = sut.configChange(rmThumbs) + m, _ = sut.configChange(cfgRmThumbs) if m.RemoveImages || !m.AdvancedEditing || !m.ShowAdd { t.Errorf("Expected RemoveImages to be false: %v", *m.Config) } *sut.Config = allTrue - m, _ = sut.configChange(advEdit) + m, _ = sut.configChange(cfgAdvEdit) if m.AdvancedEditing || !m.RemoveImages || !m.ShowAdd { t.Errorf("Expected AdvancedEditing to be false: %v", *m.Config) } diff --git a/pkg/util/func.go b/pkg/util/func.go index bf00c2f..640b95d 100644 --- a/pkg/util/func.go +++ b/pkg/util/func.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "errors" "fmt" + "os" + "path/filepath" "strings" ) @@ -39,3 +41,35 @@ func HexStringTransform(s string) (uint32, error) { return binary.BigEndian.Uint32(h), nil } + +// GetRoot finds the path to the Pocket root dir. +// If an argument was passed, it uses that. +// If an argument wasn't passed, it uses the current directory. +func GetRoot() (string, error) { + var d string + var err error + switch len(os.Args) { + case 1: + if d, err = os.Executable(); err != nil { + return "", err + } + d = filepath.Dir(d) + case 2: + d = os.Args[1] + default: + } + + d, err = filepath.Abs(d) + if err != nil { + return "", err + } + + fi, err := os.Stat(d) + if err != nil { + return "", err + } else if !fi.IsDir() { + return "", fmt.Errorf("%s is not a directory", d) + } + + return d, nil +}