Skip to content

Commit 07baa59

Browse files
authored
fix: improve symlink cleanup (#211)
1 parent 67047ef commit 07baa59

File tree

2 files changed

+91
-11
lines changed

2 files changed

+91
-11
lines changed

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ var Version string // Version is set by the build system
9898

9999
func main() {
100100
if Version == "" {
101-
Version = "1.35.1"
101+
Version = "1.36.0"
102102
}
103103

104104
cfg, err := config.LoadConfig()

operations.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,70 @@ func isValidSymlink(symlinkPath, targetPath string) bool {
845845
return true
846846
}
847847

848+
// isGollamaSymlink checks if a symlink was created by gollama by validating:
849+
// 1. The symlink target points to Ollama's blob storage
850+
// 2. The directory structure follows gollama's pattern (author/model/file.gguf)
851+
func isGollamaSymlink(symlinkPath, lmStudioModelsDir string) bool {
852+
// Get the symlink target
853+
target, err := os.Readlink(symlinkPath)
854+
if err != nil {
855+
return false
856+
}
857+
858+
// Check if target points to Ollama blob storage
859+
// Normalise path separators for cross-platform compatibility
860+
normalisedTarget := filepath.ToSlash(target)
861+
isOllamaBlob := strings.Contains(normalisedTarget, "/.ollama/models/blobs/")
862+
863+
if !isOllamaBlob {
864+
return false
865+
}
866+
867+
// Check if it follows gollama's directory structure: author/model/file.gguf
868+
relPath, err := filepath.Rel(lmStudioModelsDir, symlinkPath)
869+
if err != nil {
870+
return false
871+
}
872+
873+
pathParts := strings.Split(filepath.ToSlash(relPath), "/")
874+
if len(pathParts) != 3 {
875+
return false
876+
}
877+
878+
// Check if the filename follows gollama's pattern
879+
filename := pathParts[2]
880+
modelDir := pathParts[1]
881+
882+
// Gollama creates files like: modelname.gguf or mmproj-modelname.gguf
883+
return strings.HasSuffix(filename, ".gguf") &&
884+
(strings.HasPrefix(filename, modelDir) || strings.HasPrefix(filename, "mmproj-"+modelDir))
885+
}
886+
887+
// isBrokenSymlink checks if a symlink is broken (target doesn't exist)
888+
func isBrokenSymlink(symlinkPath string) bool {
889+
target, err := os.Readlink(symlinkPath)
890+
if err != nil {
891+
// Only treat as broken if the symlink itself doesn't exist
892+
if os.IsNotExist(err) {
893+
return true
894+
}
895+
// For permission errors or other issues, don't consider it broken
896+
logging.DebugLogger.Printf("Error reading symlink %s: %v (not considering broken)\n", symlinkPath, err)
897+
return false
898+
}
899+
900+
// Check if target exists
901+
_, err = os.Stat(target)
902+
if err == nil {
903+
return false // Target exists, symlink is not broken
904+
}
905+
if os.IsNotExist(err) {
906+
return true // Target does not exist, symlink is broken
907+
}
908+
// For other stat errors (permission, etc.), don't consider broken
909+
return false
910+
}
911+
848912
func cleanBrokenSymlinks(lmStudioModelsDir string) {
849913
err := filepath.Walk(lmStudioModelsDir, func(path string, info os.FileInfo, err error) error {
850914
if err != nil {
@@ -863,12 +927,10 @@ func cleanBrokenSymlinks(lmStudioModelsDir string) {
863927
}
864928
}
865929
} else if info.Mode()&os.ModeSymlink != 0 {
866-
linkPath, err := os.Readlink(path)
867-
if err != nil {
868-
return err
869-
}
870-
if !isValidSymlink(path, linkPath) {
871-
logging.InfoLogger.Printf("Removing invalid symlink: %s\n", path)
930+
// Only remove truly broken symlinks (where target doesn't exist)
931+
// Don't remove valid symlinks from other tools
932+
if isBrokenSymlink(path) {
933+
logging.InfoLogger.Printf("Removing broken symlink: %s\n", path)
872934
err = os.Remove(path)
873935
if err != nil {
874936
return err
@@ -992,10 +1054,28 @@ func cleanupSymlinkedModels(lmStudioModelsDir string) {
9921054
hasEmptyDir = true
9931055
}
9941056
} else if info.Mode()&os.ModeSymlink != 0 {
995-
logging.InfoLogger.Printf("Removing symlinked model: %s\n", path)
996-
err = os.Remove(path)
997-
if err != nil {
998-
return err
1057+
// Only remove symlinks that are either:
1058+
// 1. Created by gollama, or
1059+
// 2. Broken (safe to remove)
1060+
shouldRemove := false
1061+
reason := ""
1062+
1063+
if isBrokenSymlink(path) {
1064+
shouldRemove = true
1065+
reason = "broken symlink"
1066+
} else if isGollamaSymlink(path, lmStudioModelsDir) {
1067+
shouldRemove = true
1068+
reason = "gollama-created symlink"
1069+
}
1070+
1071+
if shouldRemove {
1072+
logging.InfoLogger.Printf("Removing %s: %s\n", reason, path)
1073+
err = os.Remove(path)
1074+
if err != nil {
1075+
return err
1076+
}
1077+
} else {
1078+
logging.DebugLogger.Printf("Preserving non-gollama symlink: %s\n", path)
9991079
}
10001080
}
10011081
return nil

0 commit comments

Comments
 (0)