Skip to content

Commit 382474e

Browse files
mudlerCopilot
andauthored
fix: do not delete files if used by other configured models (#7235)
* WIP Signed-off-by: Ettore Di Giacinto <[email protected]> * fix: prevent deletion of model files shared by multiple configurations (#7317) * Initial plan * fix: do not delete files if used by other configured models - Fixed bug in DeleteModelFromSystem where OR was used instead of AND for file suffix check - Fixed bug where model config filename comparison was incorrect - Added comprehensive Ginkgo test to verify shared model files are not deleted Co-authored-by: mudler <[email protected]> * fix: prevent deletion of model files shared by multiple configurations Co-authored-by: mudler <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mudler <[email protected]> Signed-off-by: Ettore Di Giacinto <[email protected]> --------- Signed-off-by: Ettore Di Giacinto <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: mudler <[email protected]>
1 parent 5fed9c6 commit 382474e

File tree

2 files changed

+165
-21
lines changed

2 files changed

+165
-21
lines changed

core/gallery/models.go

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
"slices"
910
"strings"
1011

1112
"dario.cat/mergo"
@@ -293,21 +294,32 @@ func GetLocalModelConfiguration(basePath string, name string) (*ModelConfig, err
293294
return ReadConfigFile[ModelConfig](galleryFile)
294295
}
295296

296-
func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
297-
additionalFiles := []string{}
297+
func listModelFiles(systemState *system.SystemState, name string) ([]string, error) {
298298

299299
configFile := filepath.Join(systemState.Model.ModelsPath, fmt.Sprintf("%s.yaml", name))
300300
if err := utils.VerifyPath(configFile, systemState.Model.ModelsPath); err != nil {
301-
return fmt.Errorf("failed to verify path %s: %w", configFile, err)
301+
return nil, fmt.Errorf("failed to verify path %s: %w", configFile, err)
302+
}
303+
304+
// os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths.
305+
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
306+
307+
galleryFile := filepath.Join(systemState.Model.ModelsPath, galleryFileName(name))
308+
if err := utils.VerifyPath(galleryFile, systemState.Model.ModelsPath); err != nil {
309+
return nil, fmt.Errorf("failed to verify path %s: %w", galleryFile, err)
302310
}
311+
312+
additionalFiles := []string{}
313+
allFiles := []string{}
314+
303315
// Galleryname is the name of the model in this case
304316
dat, err := os.ReadFile(configFile)
305317
if err == nil {
306318
modelConfig := &lconfig.ModelConfig{}
307319

308320
err = yaml.Unmarshal(dat, &modelConfig)
309321
if err != nil {
310-
return err
322+
return nil, err
311323
}
312324
if modelConfig.Model != "" {
313325
additionalFiles = append(additionalFiles, modelConfig.ModelFileName())
@@ -318,26 +330,15 @@ func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
318330
}
319331
}
320332

321-
// os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths.
322-
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
323-
324-
galleryFile := filepath.Join(systemState.Model.ModelsPath, galleryFileName(name))
325-
if err := utils.VerifyPath(galleryFile, systemState.Model.ModelsPath); err != nil {
326-
return fmt.Errorf("failed to verify path %s: %w", galleryFile, err)
327-
}
328-
329-
var filesToRemove []string
330-
331-
// Delete all the files associated to the model
332333
// read the model config
333334
galleryconfig, err := ReadConfigFile[ModelConfig](galleryFile)
334335
if err == nil && galleryconfig != nil {
335336
for _, f := range galleryconfig.Files {
336337
fullPath := filepath.Join(systemState.Model.ModelsPath, f.Filename)
337338
if err := utils.VerifyPath(fullPath, systemState.Model.ModelsPath); err != nil {
338-
return fmt.Errorf("failed to verify path %s: %w", fullPath, err)
339+
return allFiles, fmt.Errorf("failed to verify path %s: %w", fullPath, err)
339340
}
340-
filesToRemove = append(filesToRemove, fullPath)
341+
allFiles = append(allFiles, fullPath)
341342
}
342343
} else {
343344
log.Error().Err(err).Msgf("failed to read gallery file %s", configFile)
@@ -346,18 +347,68 @@ func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
346347
for _, f := range additionalFiles {
347348
fullPath := filepath.Join(filepath.Join(systemState.Model.ModelsPath, f))
348349
if err := utils.VerifyPath(fullPath, systemState.Model.ModelsPath); err != nil {
349-
return fmt.Errorf("failed to verify path %s: %w", fullPath, err)
350+
return allFiles, fmt.Errorf("failed to verify path %s: %w", fullPath, err)
350351
}
351-
filesToRemove = append(filesToRemove, fullPath)
352+
allFiles = append(allFiles, fullPath)
352353
}
353354

354-
filesToRemove = append(filesToRemove, galleryFile)
355+
allFiles = append(allFiles, galleryFile)
355356

356357
// skip duplicates
357-
filesToRemove = utils.Unique(filesToRemove)
358+
allFiles = utils.Unique(allFiles)
359+
360+
return allFiles, nil
361+
}
362+
363+
func DeleteModelFromSystem(systemState *system.SystemState, name string) error {
364+
configFile := filepath.Join(systemState.Model.ModelsPath, fmt.Sprintf("%s.yaml", name))
365+
366+
filesToRemove, err := listModelFiles(systemState, name)
367+
if err != nil {
368+
return err
369+
}
370+
371+
allOtherFiles := []string{}
372+
// Get all files of all other models
373+
fi, err := os.ReadDir(systemState.Model.ModelsPath)
374+
if err != nil {
375+
return err
376+
}
377+
for _, f := range fi {
378+
if f.IsDir() {
379+
continue
380+
}
381+
if strings.HasPrefix(f.Name(), "._gallery_") {
382+
continue
383+
}
384+
if !strings.HasSuffix(f.Name(), ".yaml") && !strings.HasSuffix(f.Name(), ".yml") {
385+
continue
386+
}
387+
if f.Name() == fmt.Sprintf("%s.yaml", name) || f.Name() == fmt.Sprintf("%s.yml", name) {
388+
continue
389+
}
390+
391+
name := strings.TrimSuffix(f.Name(), ".yaml")
392+
name = strings.TrimSuffix(name, ".yml")
393+
394+
log.Debug().Msgf("Checking file %s", f.Name())
395+
files, err := listModelFiles(systemState, name)
396+
if err != nil {
397+
log.Debug().Err(err).Msgf("failed to list files for model %s", f.Name())
398+
continue
399+
}
400+
allOtherFiles = append(allOtherFiles, files...)
401+
}
402+
403+
log.Debug().Msgf("Files to remove: %+v", filesToRemove)
404+
log.Debug().Msgf("All other files: %+v", allOtherFiles)
358405

359406
// Removing files
360407
for _, f := range filesToRemove {
408+
if slices.Contains(allOtherFiles, f) {
409+
log.Debug().Msgf("Skipping file %s because it is part of another model", f)
410+
continue
411+
}
361412
if e := os.Remove(f); e != nil {
362413
log.Error().Err(e).Msgf("failed to remove file %s", f)
363414
}

core/gallery/models_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,5 +183,98 @@ var _ = Describe("Model test", func() {
183183
_, err = InstallModel(context.TODO(), systemState, "../../../foo", c, map[string]interface{}{}, func(string, string, string, float64) {}, true)
184184
Expect(err).To(HaveOccurred())
185185
})
186+
187+
It("does not delete shared model files when one config is deleted", func() {
188+
tempdir, err := os.MkdirTemp("", "test")
189+
Expect(err).ToNot(HaveOccurred())
190+
defer os.RemoveAll(tempdir)
191+
192+
systemState, err := system.GetSystemState(
193+
system.WithModelPath(tempdir),
194+
)
195+
Expect(err).ToNot(HaveOccurred())
196+
197+
// Create a shared model file
198+
sharedModelFile := filepath.Join(tempdir, "shared_model.bin")
199+
err = os.WriteFile(sharedModelFile, []byte("fake model content"), 0600)
200+
Expect(err).ToNot(HaveOccurred())
201+
202+
// Create first model configuration
203+
config1 := `name: model1
204+
model: shared_model.bin`
205+
err = os.WriteFile(filepath.Join(tempdir, "model1.yaml"), []byte(config1), 0600)
206+
Expect(err).ToNot(HaveOccurred())
207+
208+
// Create first model's gallery file
209+
galleryConfig1 := ModelConfig{
210+
Name: "model1",
211+
Files: []File{
212+
{Filename: "shared_model.bin"},
213+
},
214+
}
215+
galleryData1, err := yaml.Marshal(galleryConfig1)
216+
Expect(err).ToNot(HaveOccurred())
217+
err = os.WriteFile(filepath.Join(tempdir, "._gallery_model1.yaml"), galleryData1, 0600)
218+
Expect(err).ToNot(HaveOccurred())
219+
220+
// Create second model configuration sharing the same model file
221+
config2 := `name: model2
222+
model: shared_model.bin`
223+
err = os.WriteFile(filepath.Join(tempdir, "model2.yaml"), []byte(config2), 0600)
224+
Expect(err).ToNot(HaveOccurred())
225+
226+
// Create second model's gallery file
227+
galleryConfig2 := ModelConfig{
228+
Name: "model2",
229+
Files: []File{
230+
{Filename: "shared_model.bin"},
231+
},
232+
}
233+
galleryData2, err := yaml.Marshal(galleryConfig2)
234+
Expect(err).ToNot(HaveOccurred())
235+
err = os.WriteFile(filepath.Join(tempdir, "._gallery_model2.yaml"), galleryData2, 0600)
236+
Expect(err).ToNot(HaveOccurred())
237+
238+
// Verify both configurations exist
239+
_, err = os.Stat(filepath.Join(tempdir, "model1.yaml"))
240+
Expect(err).ToNot(HaveOccurred())
241+
_, err = os.Stat(filepath.Join(tempdir, "model2.yaml"))
242+
Expect(err).ToNot(HaveOccurred())
243+
244+
// Verify the shared model file exists
245+
_, err = os.Stat(sharedModelFile)
246+
Expect(err).ToNot(HaveOccurred())
247+
248+
// Delete the first model
249+
err = DeleteModelFromSystem(systemState, "model1")
250+
Expect(err).ToNot(HaveOccurred())
251+
252+
// Verify the first configuration is deleted
253+
_, err = os.Stat(filepath.Join(tempdir, "model1.yaml"))
254+
Expect(err).To(HaveOccurred())
255+
Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue())
256+
257+
// Verify the shared model file still exists (not deleted because model2 still uses it)
258+
_, err = os.Stat(sharedModelFile)
259+
Expect(err).ToNot(HaveOccurred(), "shared model file should not be deleted when used by other configs")
260+
261+
// Verify the second configuration still exists
262+
_, err = os.Stat(filepath.Join(tempdir, "model2.yaml"))
263+
Expect(err).ToNot(HaveOccurred())
264+
265+
// Now delete the second model
266+
err = DeleteModelFromSystem(systemState, "model2")
267+
Expect(err).ToNot(HaveOccurred())
268+
269+
// Verify the second configuration is deleted
270+
_, err = os.Stat(filepath.Join(tempdir, "model2.yaml"))
271+
Expect(err).To(HaveOccurred())
272+
Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue())
273+
274+
// Verify the shared model file is now deleted (no more references)
275+
_, err = os.Stat(sharedModelFile)
276+
Expect(err).To(HaveOccurred(), "shared model file should be deleted when no configs reference it")
277+
Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue())
278+
})
186279
})
187280
})

0 commit comments

Comments
 (0)