feat(files): read and write a home file for the host

The host must write a confirmed change as the owner, in one titled
change set whose revision it can undo later. Add ReadHome and
WriteHome, which resolve the path inside the home and refuse a path
that leaves it.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 09:26:21 +00:00
1 parent 26f79638f2
commit b1df05a800
2 files changed
+118

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
package files
import (
"context"
"errors"
"strings"
"github.com/mudler/LocalAI/core/services/studio/workspace"
)
// ReadHome returns the text of a file in the home of principal. rel is a path
// inside the home. ok is false when the file does not exist. It reads as the
// user without a chat fence, for the host's own checks.
func (s *Service) ReadHome(ctx context.Context, principal, rel string, max int64) (text string, ok bool, err error) {
if !s.Available() {
return "", false, ErrUnavailable
}
abs, err := s.Abs(principal, rel)
if err != nil {
return "", false, err
}
text, _, err = s.readText(ctx, s.Actor(principal), abs, max)
if errors.Is(err, workspace.ErrNotFound) {
return "", false, nil
}
if err != nil {
return "", false, err
}
return text, true, nil
}
// WriteHome replaces a file in the home of principal as one change set with
// the given title, and returns its revision, so the caller can undo it. It
// writes as the user: it is for a change the user confirmed. The content may
// be at most max bytes.
func (s *Service) WriteHome(ctx context.Context, principal, rel, content, title string, max int64) (int64, error) {
if !s.Available() {
return 0, ErrUnavailable
}
abs, err := s.Abs(principal, rel)
if err != nil {
return 0, err
}
if err := s.EnsureHome(ctx, principal); err != nil {
return 0, err
}
title = strings.TrimSpace(title)
a := s.Actor(principal)
tx, err := s.ws.Begin(ctx, a, title)
if err != nil {
return 0, err
}
n, err := tx.Put(ctx, abs, strings.NewReader(content), workspace.PutOptions{Overwrite: true, MaxSize: max, Title: title})
if err != nil {
_ = tx.Rollback()
return 0, err
}
cs, err := tx.Commit()
if err != nil && !errors.Is(err, workspace.ErrCommitted) {
return 0, err
}
n.Rev = cs.Rev
s.Written(ctx, n, principal)
return cs.Rev, nil
}
@@ -0,0 +1,53 @@
package files_test
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/services/studio/session"
"github.com/mudler/LocalAI/core/services/studio/workspace"
)
var _ = Describe("home files for the host", func() {
It("writes a file as one change set that can be undone, and reads it back", func() {
ctx := context.Background()
f := newFiles()
Expect(f.EnsureHome(ctx, "alice")).To(Succeed())
text, ok, err := f.ReadHome(ctx, "alice", "Memory/index.md", 1024)
Expect(err).NotTo(HaveOccurred())
Expect(ok).To(BeFalse(), "text = %q", text)
rev, err := f.WriteHome(ctx, "alice", "Memory/index.md", "- home: Rome\n", "Memory: home", 1024)
Expect(err).NotTo(HaveOccurred())
Expect(rev).To(BeNumerically(">", 0))
text, ok, err = f.ReadHome(ctx, "alice", "Memory/index.md", 1024)
Expect(err).NotTo(HaveOccurred())
Expect(ok).To(BeTrue())
Expect(text).To(Equal("- home: Rome\n"))
hist, err := f.Store().History(ctx, f.Actor("alice"), workspace.HistoryQuery{Path: session.HomePath("alice"), Limit: 5})
Expect(err).NotTo(HaveOccurred())
Expect(hist).NotTo(BeEmpty())
Expect(hist[0].Title).To(Equal("Memory: home"))
Expect(hist[0].Rev).To(Equal(rev))
_, _, err = f.Store().Undo(ctx, f.Actor("alice"), rev, workspace.UndoOptions{})
Expect(err).NotTo(HaveOccurred())
_, ok, _ = f.ReadHome(ctx, "alice", "Memory/index.md", 1024)
Expect(ok).To(BeFalse(), "the undo should remove the file the change created")
})
It("refuses a path outside the home, a bigger file, and another user's home", func() {
ctx := context.Background()
f := newFiles()
Expect(f.EnsureHome(ctx, "alice")).To(Succeed())
_, err := f.WriteHome(ctx, "alice", "../bob/x.md", "x", "t", 1024)
Expect(err).To(MatchError(workspace.ErrBadName))
_, err = f.WriteHome(ctx, "alice", "Memory/big.md", "0123456789", "t", 4)
Expect(err).To(HaveOccurred())
_, _, err = f.ReadHome(ctx, "alice", "../x", 10)
Expect(err).To(MatchError(workspace.ErrBadName))
})
})