package paths import ( "fmt" "io" "os" "path/filepath" ) const appDirName = "PrivateVoice Input" var legacyAppDirNames = []string{"VoiceSnap"} // AppSupportDir returns the per-user persistent data directory. func AppSupportDir() string { return appSupportDir(appDirName) } func appSupportDir(dirName string) string { base, err := os.UserConfigDir() if err != nil || base == "" { home, homeErr := os.UserHomeDir() if homeErr != nil || home == "" { return dirName } base = filepath.Join(home, "Library", "Application Support") } return filepath.Join(base, dirName) } // File returns a path inside the persistent app data directory. func File(name string) string { return filepath.Join(AppSupportDir(), name) } // ModelDir returns the legacy persistent SenseVoice model directory. func ModelDir() string { return filepath.Join(AppSupportDir(), "models", "sensevoice") } // ModelsRoot returns the persistent model root directory. func ModelsRoot() string { return filepath.Join(AppSupportDir(), "models") } // ModelInstallDir returns the persistent install directory for a model profile. func ModelInstallDir(name string) string { return filepath.Join(ModelsRoot(), name) } // ModelStatePath returns the persistent model install state file path. func ModelStatePath() string { return filepath.Join(ModelsRoot(), "state.json") } // Ensure creates the app data directory. func Ensure() error { return os.MkdirAll(AppSupportDir(), 0755) } // MigrateFromLegacyExecutableDir copies old user data into the current app // support directory. Existing new-location files are never overwritten. func MigrateFromLegacyExecutableDir() error { if err := Ensure(); err != nil { return err } for _, dirName := range legacyAppDirNames { legacyDir := appSupportDir(dirName) if samePath(legacyDir, AppSupportDir()) { continue } if err := copyUserDataFromDir(legacyDir); err != nil { return fmt.Errorf("migrate %s: %w", dirName, err) } } legacyDir, err := legacyExecutableDir() if err != nil { return nil } if samePath(legacyDir, AppSupportDir()) { return nil } if err := copyUserDataFromDir(legacyDir); err != nil { return err } return nil } func copyUserDataFromDir(sourceDir string) error { for _, name := range []string{"config.json", "userdict.json", "history.json"} { if err := copyFileIfMissing(filepath.Join(sourceDir, name), File(name)); err != nil { return fmt.Errorf("migrate %s: %w", name, err) } } if err := copyDirIfMissing(filepath.Join(sourceDir, "models"), ModelsRoot()); err != nil { return fmt.Errorf("migrate models: %w", err) } return nil } func legacyExecutableDir() (string, error) { exe, err := os.Executable() if err != nil { return "", err } return filepath.Dir(exe), nil } func copyFileIfMissing(src, dst string) error { info, err := os.Stat(src) if err != nil { if os.IsNotExist(err) { return nil } return err } if info.IsDir() { return nil } if _, err := os.Stat(dst); err == nil { return nil } else if !os.IsNotExist(err) { return err } if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_EXCL, info.Mode()) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } return out.Close() } func copyDirIfMissing(src, dst string) error { info, err := os.Stat(src) if err != nil { if os.IsNotExist(err) { return nil } return err } if !info.IsDir() { return nil } if _, err := os.Stat(dst); err == nil { return nil } else if !os.IsNotExist(err) { return err } return filepath.WalkDir(src, func(path string, entry os.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } rel, err := filepath.Rel(src, path) if err != nil { return err } target := filepath.Join(dst, rel) info, err := entry.Info() if err != nil { return err } if entry.IsDir() { return os.MkdirAll(target, info.Mode()) } return copyFileIfMissing(path, target) }) } func samePath(a, b string) bool { aa, errA := filepath.EvalSymlinks(a) bb, errB := filepath.EvalSymlinks(b) if errA == nil { a = aa } if errB == nil { b = bb } absA, errA := filepath.Abs(a) absB, errB := filepath.Abs(b) if errA == nil { a = absA } if errB == nil { b = absB } return a == b }