package sugar import ( "fmt" "log" "log/slog" "os" "path/filepath" ) // EnsureDir joins the given path segments and creates the directory // (including all parent directories) if it does not already exist. // Returns the resolved path or an error if creation fails. func EnsureDir(dirPath ...string) (string, error) { outPath := filepath.Join(dirPath...) if outPath == "" { return "", fmt.Errorf("path must not be empty") } if err := os.MkdirAll(outPath, os.FileMode(0775)); err != nil { return "", fmt.Errorf("failed to ensure directory %q: %w", outPath, err) } return outPath, nil } // ExecDir returns the directory of the running binary. // Works correctly even with symlinks and -trimpath builds. func ExecDir() (string, error) { exe, err := os.Executable() if err != nil { return "", err } // EvalSymlinks is critical — os.Executable can return a symlink path resolved, err := filepath.EvalSymlinks(exe) if err != nil { return "", err } return filepath.Dir(resolved), nil } func WriteDataToTmpFile(content, fp string) (int, string, error) { dir, err := EnsureDir(os.TempDir(), "mdb") if err != nil { slog.Error("EnsureDir", "err", err.Error()) return 0, dir, err } fp = filepath.Join(dir, fp) f, err := os.Create(fp) if err != nil { slog.Error("CreateFile", "e", err) return 0, fp, err } filesize, err := f.WriteString(content) if err != nil { slog.Error("WriteString", "e", err) return 0, fp, err } err = f.Close() if err != nil { slog.Error("CloseFile", "e", err) return 0, fp, err } return filesize, fp, nil } // ConfigPaths returns candidate directories for config lookup. // Priority: XDG_CONFIG_HOME → binary dir → CWD → /etc/appname func ConfigPaths(appName string) []string { var candidates []string // 1. Explicit env override (highest priority) if envPath := os.Getenv("APP_CONFIG_DIR"); envPath != "" { candidates = append(candidates, envPath) } // 2. Relative to binary (best for production deployments) if exe, err := os.Executable(); err == nil { if resolved, err := filepath.EvalSymlinks(exe); err == nil { candidates = append(candidates, filepath.Dir(resolved)) } } // 3. CWD (convenient for `go run` and development) if cwd, err := os.Getwd(); err == nil { candidates = append(candidates, cwd) } // 4. XDG standard (Linux/Unix) if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { candidates = append(candidates, filepath.Join(xdg, appName)) } else if home, err := os.UserHomeDir(); err == nil { candidates = append(candidates, filepath.Join(home, ".config", appName)) } // 5. System-wide fallback candidates = append(candidates, filepath.Join("/etc", appName)) return candidates } // FindConfig searches all candidate dirs for the given filename. func FindConfig(appName, filename string) (string, error) { for _, dir := range ConfigPaths(appName) { path := filepath.Join(dir, filename) if _, err := os.Stat(path); err == nil { return path, nil } } return "", fmt.Errorf("config %q not found in any candidate directory", filename) } // DebugPath Debug helper — remove before production func DebugPath(path string) { info, err := os.Stat(path) log.Printf("path=%q exists=%v err=%v mode=%v", path, err == nil, err, func() string { if info != nil { return info.Mode().String() } return "n/a" }()) }