diff --git a/core/services/studio/files/homefile.go b/core/services/studio/files/homefile.go new file mode 100644 index 0000000000..6482a4ec48 --- /dev/null +++ b/core/services/studio/files/homefile.go @@ -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 +} diff --git a/core/services/studio/files/homefile_test.go b/core/services/studio/files/homefile_test.go new file mode 100644 index 0000000000..6e5d188fcb --- /dev/null +++ b/core/services/studio/files/homefile_test.go @@ -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)) + }) +})