Skip to content

Commit ee02a25

Browse files
authored
fix: improve symlink cleanup (#213)
1 parent 25026e5 commit ee02a25

File tree

3 files changed

+124
-2
lines changed

3 files changed

+124
-2
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.36.1"
101+
Version = "1.37.0"
102102
}
103103

104104
cfg, err := config.LoadConfig()

operations.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,9 +879,15 @@ func isGollamaSymlink(symlinkPath, lmStudioModelsDir string) bool {
879879
filename := pathParts[2]
880880
modelDir := pathParts[1]
881881

882+
// Strip -GGUF suffix from modelDir if present (common in LM Studio imports)
883+
baseModelName := modelDir
884+
if strings.HasSuffix(modelDir, "-GGUF") {
885+
baseModelName = modelDir[:len(modelDir)-5]
886+
}
887+
882888
// Gollama creates files like: modelname.gguf or mmproj-modelname.gguf
883889
return strings.HasSuffix(filename, ".gguf") &&
884-
(strings.HasPrefix(filename, modelDir) || strings.HasPrefix(filename, "mmproj-"+modelDir))
890+
(strings.HasPrefix(filename, baseModelName) || strings.HasPrefix(filename, "mmproj-"+baseModelName))
885891
}
886892

887893
// isBrokenSymlink checks if a symlink is broken (target doesn't exist)

operations_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
44
"os"
5+
"path/filepath"
6+
"strings"
57
"testing"
68

79
"github.com/sammcj/gollama/config"
@@ -59,3 +61,117 @@ func TestRunModel(t *testing.T) {
5961
}
6062
}
6163
}
64+
65+
func TestIsGollamaSymlink(t *testing.T) {
66+
lmStudioModelsDir := "/Users/test/.lmstudio/models"
67+
68+
tests := []struct {
69+
name string
70+
symlinkPath string
71+
target string
72+
expected bool
73+
}{
74+
{
75+
name: "Valid gollama symlink with -GGUF suffix",
76+
symlinkPath: "/Users/test/.lmstudio/models/unknown/nomic-embed-text-latest-GGUF/nomic-embed-text-latest.gguf",
77+
target: "/Users/test/.ollama/models/blobs/sha256-970aa74c0a90ef7482477cf803618e776e173c007bf957f635f1015bfcfef0e6",
78+
expected: true,
79+
},
80+
{
81+
name: "Valid gollama symlink with -GGUF suffix different model",
82+
symlinkPath: "/Users/test/.lmstudio/models/unknown/qwen3-0.6b-GGUF/qwen3-0.6b.gguf",
83+
target: "/Users/test/.ollama/models/blobs/sha256-7f4030143c1c477224c5434f8272c662a8b042079a0a584f0a27a1684fe2e1fa",
84+
expected: true,
85+
},
86+
{
87+
name: "Valid gollama symlink without -GGUF suffix",
88+
symlinkPath: "/Users/test/.lmstudio/models/testauthor/testmodel/testmodel.gguf",
89+
target: "/Users/test/.ollama/models/blobs/sha256-abc123",
90+
expected: true,
91+
},
92+
{
93+
name: "Valid mmproj symlink with -GGUF suffix",
94+
symlinkPath: "/Users/test/.lmstudio/models/unknown/qwen3-0.6b-GGUF/mmproj-qwen3-0.6b.gguf",
95+
target: "/Users/test/.ollama/models/blobs/sha256-def456",
96+
expected: true,
97+
},
98+
{
99+
name: "Non-Ollama blob target",
100+
symlinkPath: "/Users/test/.lmstudio/models/testauthor/testmodel/testmodel.gguf",
101+
target: "/some/other/path/model.gguf",
102+
expected: false,
103+
},
104+
{
105+
name: "Wrong directory structure (too many parts)",
106+
symlinkPath: "/Users/test/.lmstudio/models/testauthor/testmodel/subdir/testmodel.gguf",
107+
target: "/Users/test/.ollama/models/blobs/sha256-abc123",
108+
expected: false,
109+
},
110+
{
111+
name: "Wrong filename (doesn't match model name)",
112+
symlinkPath: "/Users/test/.lmstudio/models/testauthor/testmodel-GGUF/wrongname.gguf",
113+
target: "/Users/test/.ollama/models/blobs/sha256-abc123",
114+
expected: false,
115+
},
116+
{
117+
name: "Non-gguf file",
118+
symlinkPath: "/Users/test/.lmstudio/models/testauthor/testmodel/testmodel.bin",
119+
target: "/Users/test/.ollama/models/blobs/sha256-abc123",
120+
expected: false,
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
// Create a temporary directory structure and symlink
127+
tempDir := t.TempDir()
128+
testLMDir := filepath.Join(tempDir, "lmstudio", "models")
129+
130+
// Replace the test path in symlinkPath with our temp path
131+
relPath, _ := filepath.Rel(lmStudioModelsDir, tt.symlinkPath)
132+
actualSymlinkPath := filepath.Join(testLMDir, relPath)
133+
134+
// Create parent directories
135+
err := os.MkdirAll(filepath.Dir(actualSymlinkPath), 0755)
136+
if err != nil {
137+
t.Fatalf("Failed to create directory: %v", err)
138+
}
139+
140+
// Create the target file (so the symlink won't be broken)
141+
var actualTarget string
142+
if strings.Contains(tt.target, ".ollama/models/blobs/") {
143+
// For Ollama blob targets
144+
targetDir := filepath.Join(tempDir, ".ollama", "models", "blobs")
145+
err = os.MkdirAll(targetDir, 0755)
146+
if err != nil {
147+
t.Fatalf("Failed to create target directory: %v", err)
148+
}
149+
actualTarget = filepath.Join(targetDir, filepath.Base(tt.target))
150+
} else {
151+
// For non-Ollama targets, create in a different location
152+
targetDir := filepath.Join(tempDir, "some", "other", "path")
153+
err = os.MkdirAll(targetDir, 0755)
154+
if err != nil {
155+
t.Fatalf("Failed to create non-Ollama target directory: %v", err)
156+
}
157+
actualTarget = filepath.Join(targetDir, filepath.Base(tt.target))
158+
}
159+
160+
err = os.WriteFile(actualTarget, []byte("test"), 0644)
161+
if err != nil {
162+
t.Fatalf("Failed to create target file: %v", err)
163+
}
164+
165+
// Create symlink
166+
err = os.Symlink(actualTarget, actualSymlinkPath)
167+
if err != nil {
168+
t.Fatalf("Failed to create symlink: %v", err)
169+
}
170+
171+
result := isGollamaSymlink(actualSymlinkPath, testLMDir)
172+
if result != tt.expected {
173+
t.Errorf("isGollamaSymlink() = %v, expected %v", result, tt.expected)
174+
}
175+
})
176+
}
177+
}

0 commit comments

Comments
 (0)