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
|
}
|