8000 hub: Resolve symlinks in Repo.Save / Repo.SaveCtx when linkOnly=true by janpfeifer · Pull Request #70 · gomlx/go-huggingface · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- 2026/10/30:
- `hub`: Resolve symlinks via `filepath.EvalSymlinks` in `Repo.Save` / `Repo.SaveCtx` when `linkOnly=true` so hard links point to the underlying blob files rather than copying relative snapshot symlinks.

- 2026/10/12:
- `models/transformer`: Support saving and reloading fine-tuned models with GoMLX checkpoints and embedded filesystems.
- Added `Model.SaveConfigs(dir)` and `Model.SaveToCheckpoint(checkpointDir)` to export model and tokenizer configurations into a standalone directory or into the `hf/` sub-directory of a GoMLX checkpoint.
Expand Down
14 changes: 8 additions & 6 deletions hub/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,17 @@ func (r *Repo) SaveCtx(ctx context.Context, dirPath string, linkOnly bool) error
return errors.Wrapf(err, "failed to create directory for %q", dst)
}

if files.Exists(dst) {
if err := os.Remove(dst); err != nil {
return errors.Wrapf(err, "failed to remove existing file %q 8000 before overwriting", dst)
}
if err := os.Remove(dst); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrapf(err, "failed to remove existing file %q before overwriting", dst)
}

if linkOnly {
if err := os.Link(src, dst); err != nil {
return errors.Wrapf(err, "failed to create hard link from %q to %q", src, dst)
realSrc, err := filepath.EvalSymlinks(src)
if err != nil {
return errors.Wrapf(err, "failed to resolve symlink %q", src)
}
if err := os.Link(realSrc, dst); err != nil {
return errors.Wrapf(err, "failed to create hard link from %q to %q", realSrc, dst)
}
} else {
if err := copyFile(src, dst); err != nil {
Expand Down
38 changes: 38 additions & 0 deletions hub/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,41 @@ func TestDeleteCache_LocalRepoFails(t *testing.T) {
assert.Contains(t, err.Error(), "cannot DeleteCache of a local repository")
}

func TestSave_LinkOnlyResolvesSymlinks(t *testing.T) {
tempCacheDir := t.TempDir()
repo := New("test/model").
WithCacheDir(tempCacheDir).
WithEndpoint("http://invalid.unreachable.endpoint")

cacheDir, err := repo.repoCacheDir()
require.NoError(t, err)

commitHash := "0123456789abcdef0123456789abcdef01234567"
infoDir := filepath.Join(cacheDir, "info")
require.NoError(t, os.MkdirAll(infoDir, 0755))
infoJSON := `{"id":"test/model","sha":"` + commitHash + `","siblings":[{"rfilename":"config.json","size":19}]}`
require.NoError(t, os.WriteFile(filepath.Join(infoDir, "main"), []byte(infoJSON), 0644))

blobsDir := filepath.Join(cacheDir, "blobs")
require.NoError(t, os.MkdirAll(blobsDir, 0755))
blobPath := filepath.Join(blobsDir, "etag123")
require.NoError(t, os.WriteFile(blobPath, []byte(`{"model_type":"ok"}`), 0644))

snapshotDir := filepath.Join(cacheDir, "snapshots", commitHash)
require.NoError(t, os.MkdirAll(snapshotDir, 0755))
require.NoError(t, createSymLink(filepath.Join(snapshotDir, "config.json"), blobPath))

targetDir := filepath.Join(tempCacheDir, "saved")
require.NoError(t, repo.Save(targetDir, true))

savedFile := filepath.Join(targetDir, "config.json")
fi, err := os.Lstat(savedFile)
require.NoError(t, err)
assert.Zero(t, fi.Mode()&os.ModeSymlink, "saved file with linkOnly=true should be a regular hard-linked file, not a symlink")

data, err := os.ReadFile(savedFile)
require.NoError(t, err)
assert.Equal(t, `{"model_type":"ok"}`, string(data))
}


Loading
0