package model
|
|
import (
|
"fmt"
|
"os"
|
"path/filepath"
|
"strings"
|
|
"voicesnap/internal/paths"
|
)
|
|
type resolveCandidate struct {
|
dir string
|
sourceDirKind string
|
fromState bool
|
}
|
|
func ResolveModel(modelID string) (ResolvedModel, error) {
|
return ResolveModelInRoot(modelID, paths.ModelsRoot())
|
}
|
|
func ResolveModelInRoot(modelID, modelsRoot string) (ResolvedModel, error) {
|
profile, err := GetModelProfile(modelID)
|
if err != nil {
|
return ResolvedModel{}, err
|
}
|
|
result := ResolvedModel{
|
ModelID: profile.ID,
|
Profile: profile,
|
BackendKind: profile.BackendKind,
|
ProviderOrder: append([]string(nil), profile.ProviderOrder...),
|
Status: ModelNotInstalled,
|
Files: make(map[string]string),
|
}
|
|
state, stateErr := LoadInstallStateFromRoot(modelsRoot)
|
if stateErr != nil {
|
result.Problems = append(result.Problems, fmt.Sprintf("state invalid: %v", stateErr))
|
}
|
|
candidates := make([]resolveCandidate, 0, 3)
|
seen := make(map[string]bool)
|
stateCandidateFailed := stateErr != nil
|
addCandidate := func(relOrAbs, sourceDirKind string, fromState bool) {
|
if relOrAbs == "" {
|
return
|
}
|
if fromState && !validStateRelativePath(relOrAbs) {
|
stateCandidateFailed = true
|
result.Problems = append(result.Problems, fmt.Sprintf("state path escapes models root: %s", relOrAbs))
|
return
|
}
|
dir := filepath.Join(modelsRoot, relOrAbs)
|
clean := filepath.Clean(dir)
|
if seen[clean] {
|
return
|
}
|
seen[clean] = true
|
candidates = append(candidates, resolveCandidate{
|
dir: clean,
|
sourceDirKind: sourceDirKind,
|
fromState: fromState,
|
})
|
}
|
|
if state != nil {
|
if installed, ok := state.InstalledModels[profile.ID]; ok {
|
addCandidate(installed.Path, installed.SourceDirKind, true)
|
}
|
}
|
addCandidate(profile.InstallDirName, "new", false)
|
for _, legacy := range profile.LegacyDirNames {
|
addCandidate(legacy, "legacy", false)
|
}
|
|
anyDirExists := false
|
var lastValidation ValidationResult
|
var existingInvalidValidation ValidationResult
|
|
for _, candidate := range candidates {
|
if directoryExists(candidate.dir) {
|
anyDirExists = true
|
}
|
|
validation := ValidateModelDir(profile, candidate.dir)
|
if validation.Valid {
|
result.RootDir = candidate.dir
|
result.SourceDirKind = candidate.sourceDirKind
|
result.Files = validation.Files
|
if stateCandidateFailed {
|
result.Status = ModelStateInvalidFallbackFound
|
} else {
|
result.Status = ModelInstalledValid
|
}
|
return result, nil
|
}
|
|
lastValidation = validation
|
if directoryExists(candidate.dir) && !validation.Valid && len(existingInvalidValidation.Missing) == 0 && len(existingInvalidValidation.Problems) == 0 {
|
existingInvalidValidation = validation
|
}
|
if candidate.fromState {
|
stateCandidateFailed = true
|
result.Problems = append(result.Problems, validation.Problems...)
|
}
|
}
|
|
switch {
|
case stateCandidateFailed:
|
result.Status = ModelStateInvalidNoFallback
|
case anyDirExists:
|
result.Status = ModelInstalledIncomplete
|
result.Missing = append(result.Missing, existingInvalidValidation.Missing...)
|
result.Problems = append(result.Problems, existingInvalidValidation.Problems...)
|
default:
|
result.Status = ModelNotInstalled
|
result.Missing = append(result.Missing, lastValidation.Missing...)
|
result.Problems = append(result.Problems, lastValidation.Problems...)
|
}
|
|
return result, nil
|
}
|
|
func directoryExists(path string) bool {
|
info, err := os.Stat(path)
|
return err == nil && info.IsDir()
|
}
|
|
func validStateRelativePath(path string) bool {
|
if filepath.IsAbs(path) {
|
return false
|
}
|
clean := filepath.Clean(path)
|
return clean != "." && clean != ".." && !strings.HasPrefix(clean, ".."+string(os.PathSeparator))
|
}
|