diff options
| author | Paul Buetow <paul@buetow.org> | 2025-10-02 08:38:03 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-10-02 08:38:03 +0300 |
| commit | 0c1b108ff5fccf39ae5bc6dc06802ce565bda633 (patch) | |
| tree | 914e65e04bae26d3eae565f9d6a64d08ade361d0 /internal/fsutil | |
| parent | 36be499ed342d92969ccaaff083c557a0951def9 (diff) | |
new version major refactorv0.2.0
Diffstat (limited to 'internal/fsutil')
| -rw-r--r-- | internal/fsutil/path.go | 99 | ||||
| -rw-r--r-- | internal/fsutil/path_test.go | 112 |
2 files changed, 211 insertions, 0 deletions
diff --git a/internal/fsutil/path.go b/internal/fsutil/path.go new file mode 100644 index 0000000..c872a5b --- /dev/null +++ b/internal/fsutil/path.go @@ -0,0 +1,99 @@ +package fsutil + +import ( + "errors" + "fmt" + "io/fs" + "os" + "os/user" + "path/filepath" + "strings" +) + +// ResolveRootPath expands and validates the supplied root path. When the +// caller did not specify a value, defaultValue is used and created on demand. +func ResolveRootPath(input, defaultValue string) (string, error) { + value, isDefault := normalizeRootInput(input, defaultValue) + expanded, err := expandPath(value) + if err != nil { + return "", fmt.Errorf("cannot expand root path %q: %w", value, err) + } + abs, err := filepath.Abs(expanded) + if err != nil { + return "", fmt.Errorf("cannot resolve root path %q: %w", expanded, err) + } + info, err := ensureRootExists(abs, isDefault) + if err != nil { + return "", err + } + if !info.IsDir() && !info.Mode().IsRegular() { + return "", fmt.Errorf("root path %q is not a file or directory", abs) + } + return abs, nil +} + +func normalizeRootInput(input, defaultValue string) (value string, isDefault bool) { + trimmed := strings.TrimSpace(input) + if trimmed == "" { + return defaultValue, true + } + return trimmed, false +} + +func ensureRootExists(path string, allowCreate bool) (fs.FileInfo, error) { + info, err := os.Stat(path) + if err == nil { + return info, nil + } + if !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("cannot access root path %q: %w", path, err) + } + if !allowCreate { + return nil, fmt.Errorf("root path does not exist: %s", path) + } + if mkErr := os.MkdirAll(path, 0o755); mkErr != nil { + return nil, fmt.Errorf("cannot create default directory %q: %w", path, mkErr) + } + info, err = os.Stat(path) + if err != nil { + return nil, fmt.Errorf("cannot stat default directory %q: %w", path, err) + } + return info, nil +} + +func expandPath(p string) (string, error) { + if p == "" || p[0] != '~' { + return p, nil + } + if len(p) == 1 { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return home, nil + } + if p[1] == '/' { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, p[2:]), nil + } + username, rest := splitUserPath(p) + usr, err := user.Lookup(username) + if err != nil { + return "", err + } + if rest == "" { + return usr.HomeDir, nil + } + return filepath.Join(usr.HomeDir, rest), nil +} + +func splitUserPath(p string) (string, string) { + sep := strings.IndexRune(p, '/') + if sep == -1 { + return p[1:], "" + } + return p[1:sep], p[sep:] +} diff --git a/internal/fsutil/path_test.go b/internal/fsutil/path_test.go new file mode 100644 index 0000000..4b88573 --- /dev/null +++ b/internal/fsutil/path_test.go @@ -0,0 +1,112 @@ +package fsutil + +import ( + "os" + "os/user" + "path/filepath" + "testing" +) + +func TestResolveRootPathCreatesDefault(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + root, err := ResolveRootPath("", "~/Yoga") + if err != nil { + t.Fatalf("resolve root: %v", err) + } + expected := filepath.Join(home, "Yoga") + if root != expected { + t.Fatalf("expected %s, got %s", expected, root) + } + info, err := os.Stat(expected) + if err != nil { + t.Fatalf("stat default: %v", err) + } + if !info.IsDir() { + t.Fatalf("expected directory at %s", expected) + } +} + +func TestResolveRootPathRequiresExisting(t *testing.T) { + tmp := t.TempDir() + missing := filepath.Join(tmp, "missing") + if _, err := ResolveRootPath(missing, "~/Yoga"); err == nil { + t.Fatalf("expected error for %s", missing) + } +} + +func TestResolveRootPathAllowsFile(t *testing.T) { + tmp := t.TempDir() + file := filepath.Join(tmp, "video.mp4") + if err := os.WriteFile(file, []byte("x"), 0o644); err != nil { + t.Fatalf("write file: %v", err) + } + got, err := ResolveRootPath(file, "~/Yoga") + if err != nil { + t.Fatalf("resolve root: %v", err) + } + if got != file { + t.Fatalf("expected file path returned, got %s", got) + } +} + +func TestExpandPathWithHome(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + custom := filepath.Join(home, "custom") + if err := os.MkdirAll(custom, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + got, err := ResolveRootPath("~/custom", "~/Yoga") + if err != nil { + t.Fatalf("resolve root: %v", err) + } + expected := filepath.Join(home, "custom") + if got != expected { + t.Fatalf("expected %s, got %s", expected, got) + } + if v, err := expandPath("~"); err != nil || v != home { + t.Fatalf("expandPath ~ failed: %v %s", err, v) + } + if _, err := expandPath("~no_such_user/foo"); err == nil { + t.Fatalf("expected error for unknown user") + } + if path, err := expandPath("relative/path"); err != nil || path != "relative/path" { + t.Fatalf("expected relative path unchanged, got %s %v", path, err) + } + if current, err := user.Current(); err == nil { + value := "~" + current.Username + if p, err := expandPath(value); err != nil || p != current.HomeDir { + t.Fatalf("expected home dir %s, got %s (%v)", current.HomeDir, p, err) + } + } +} + +func TestSplitUserPath(t *testing.T) { + user, rest := splitUserPath("~alice/videos") + if user != "alice" || rest != "/videos" { + t.Fatalf("unexpected split %s %s", user, rest) + } + user, rest = splitUserPath("~bob") + if user != "bob" || rest != "" { + t.Fatalf("unexpected split %s %s", user, rest) + } +} + +func TestNormalizeRootInput(t *testing.T) { + const fallback = "~/Yoga" + value, isDefault := normalizeRootInput("", fallback) + if !isDefault || value != fallback { + t.Fatalf("unexpected normalize result %s %v", value, isDefault) + } + value, isDefault = normalizeRootInput(" /tmp ", fallback) + if isDefault || value != "/tmp" { + t.Fatalf("unexpected normalize result %s %v", value, isDefault) + } +} + +func TestEnsureRootExistsErrors(t *testing.T) { + if _, err := ensureRootExists(filepath.Join(t.TempDir(), "missing"), false); err == nil { + t.Fatalf("expected error when creation not allowed") + } +} |
