mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
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:
2 files changed
+118
No files matched your search
@@ -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))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user