package paths
|
|
import (
|
"fmt"
|
"io"
|
"os"
|
"path/filepath"
|
)
|
|
const appDirName = "VoiceSnap"
|
|
// AppSupportDir returns the per-user persistent data directory.
|
func AppSupportDir() string {
|
base, err := os.UserConfigDir()
|
if err != nil || base == "" {
|
home, homeErr := os.UserHomeDir()
|
if homeErr != nil || home == "" {
|
return appDirName
|
}
|
base = filepath.Join(home, "Library", "Application Support")
|
}
|
return filepath.Join(base, appDirName)
|
}
|
|
// File returns a path inside the persistent app data directory.
|
func File(name string) string {
|
return filepath.Join(AppSupportDir(), name)
|
}
|
|
// ModelDir returns the 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")
|
}
|
|
// Ensure creates the app data directory.
|
func Ensure() error {
|
return os.MkdirAll(AppSupportDir(), 0755)
|
}
|
|
// MigrateFromLegacyExecutableDir copies old user data from beside the executable
|
// into Application Support. Existing new-location files are never overwritten.
|
func MigrateFromLegacyExecutableDir() error {
|
if err := Ensure(); err != nil {
|
return err
|
}
|
|
legacyDir, err := legacyExecutableDir()
|
if err != nil {
|
return nil
|
}
|
if samePath(legacyDir, AppSupportDir()) {
|
return nil
|
}
|
|
for _, name := range []string{"config.json", "userdict.json", "history.json"} {
|
if err := copyFileIfMissing(filepath.Join(legacyDir, name), File(name)); err != nil {
|
return fmt.Errorf("migrate %s: %w", name, err)
|
}
|
}
|
|
if err := copyDirIfMissing(filepath.Join(legacyDir, "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
|
}
|