Share immutable in-memory filesystems
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
package promptfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/readonlyfs"
|
||||
)
|
||||
|
||||
// ModulePromptFile maps a module-owned embedded prompt file into the registered
|
||||
@@ -37,7 +35,8 @@ func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
if moduleFS == nil {
|
||||
return nil, fmt.Errorf("module prompt filesystem must not be nil")
|
||||
}
|
||||
assets := make(promptMapFS, len(files)+len(sharedFiles))
|
||||
assets := make(map[string][]byte, len(files)+len(sharedFiles))
|
||||
moduleDestinations := make(map[string]struct{}, len(files))
|
||||
for _, file := range files {
|
||||
name, err := cleanPromptPath(file.Name)
|
||||
if err != nil {
|
||||
@@ -46,6 +45,11 @@ func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
if strings.Contains(name, "/") {
|
||||
return nil, fmt.Errorf("module prompt file name %q must not contain path separators", file.Name)
|
||||
}
|
||||
destination := "assets/prompts/" + cleanModuleDir + "/" + name
|
||||
if _, exists := moduleDestinations[destination]; exists {
|
||||
return nil, fmt.Errorf("duplicate module prompt destination %q", destination)
|
||||
}
|
||||
moduleDestinations[destination] = struct{}{}
|
||||
filePath, err := cleanPromptPath(file.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("module prompt file path %q: %w", file.Path, err)
|
||||
@@ -54,9 +58,10 @@ func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read module prompt asset %s: %w", filePath, err)
|
||||
}
|
||||
assets["assets/prompts/"+cleanModuleDir+"/"+name] = append([]byte(nil), data...)
|
||||
assets[destination] = data
|
||||
}
|
||||
|
||||
sharedDestinations := make(map[string]struct{}, len(sharedFiles))
|
||||
for _, file := range sharedFiles {
|
||||
name, err := cleanPromptPath(file.Name)
|
||||
if err != nil {
|
||||
@@ -65,6 +70,11 @@ func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
if strings.Contains(name, "/") {
|
||||
return nil, fmt.Errorf("shared prompt file name %q must not contain path separators", file.Name)
|
||||
}
|
||||
destination := "assets/prompts/" + cleanModuleDir + "/sharedassets/" + name
|
||||
if _, exists := sharedDestinations[destination]; exists {
|
||||
return nil, fmt.Errorf("duplicate sharedassets prompt destination %q", destination)
|
||||
}
|
||||
sharedDestinations[destination] = struct{}{}
|
||||
if file.FS == nil {
|
||||
return nil, fmt.Errorf("shared prompt file %q filesystem must not be nil", file.Name)
|
||||
}
|
||||
@@ -76,77 +86,13 @@ func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read shared prompt asset %s: %w", filePath, err)
|
||||
}
|
||||
assets["assets/prompts/"+cleanModuleDir+"/sharedassets/"+name] = append([]byte(nil), data...)
|
||||
assets[destination] = data
|
||||
}
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
type promptMapFS map[string][]byte
|
||||
|
||||
func (m promptMapFS) Open(name string) (fs.File, error) {
|
||||
cleaned, err := cleanPromptFSPath(name)
|
||||
fsys, err := readonlyfs.New(assets)
|
||||
if err != nil {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
|
||||
return nil, fmt.Errorf("construct prompt filesystem: %w", err)
|
||||
}
|
||||
if data, ok := m[cleaned]; ok {
|
||||
return &promptFile{
|
||||
reader: bytes.NewReader(data),
|
||||
info: promptFileInfo{name: path.Base(cleaned), size: int64(len(data))},
|
||||
}, nil
|
||||
}
|
||||
entries := m.dirEntries(cleaned)
|
||||
if entries != nil {
|
||||
return &promptDir{name: path.Base(cleaned), entries: entries}, nil
|
||||
}
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
|
||||
func (m promptMapFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
cleaned, err := cleanPromptFSPath(name)
|
||||
if err != nil {
|
||||
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
|
||||
}
|
||||
entries := m.dirEntries(cleaned)
|
||||
if entries == nil {
|
||||
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (m promptMapFS) dirEntries(dir string) []fs.DirEntry {
|
||||
children := map[string]promptDirEntry{}
|
||||
prefix := ""
|
||||
if dir != "." {
|
||||
prefix = dir + "/"
|
||||
}
|
||||
for name, data := range m {
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
rest := strings.TrimPrefix(name, prefix)
|
||||
if rest == "" {
|
||||
continue
|
||||
}
|
||||
childName, _, hasSlash := strings.Cut(rest, "/")
|
||||
entry := promptDirEntry{name: childName, dir: hasSlash}
|
||||
if !hasSlash {
|
||||
entry.size = int64(len(data))
|
||||
}
|
||||
children[childName] = entry
|
||||
}
|
||||
if len(children) == 0 {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(children))
|
||||
for name := range children {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
entries := make([]fs.DirEntry, 0, len(names))
|
||||
for _, name := range names {
|
||||
entries = append(entries, children[name])
|
||||
}
|
||||
return entries
|
||||
return fsys, nil
|
||||
}
|
||||
|
||||
func cleanPromptPath(name string) (string, error) {
|
||||
@@ -160,81 +106,3 @@ func cleanPromptPath(name string) (string, error) {
|
||||
}
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func cleanPromptFSPath(name string) (string, error) {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return "", fmt.Errorf("path must not be empty")
|
||||
}
|
||||
cleaned := path.Clean(strings.TrimPrefix(trimmed, "/"))
|
||||
if cleaned == "." {
|
||||
return cleaned, nil
|
||||
}
|
||||
if !fs.ValidPath(cleaned) {
|
||||
return "", fmt.Errorf("invalid path %q", name)
|
||||
}
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
type promptFile struct {
|
||||
reader *bytes.Reader
|
||||
info promptFileInfo
|
||||
}
|
||||
|
||||
func (f *promptFile) Stat() (fs.FileInfo, error) { return f.info, nil }
|
||||
func (f *promptFile) Read(p []byte) (int, error) { return f.reader.Read(p) }
|
||||
func (f *promptFile) Close() error { return nil }
|
||||
|
||||
type promptDir struct {
|
||||
name string
|
||||
offset int
|
||||
entries []fs.DirEntry
|
||||
}
|
||||
|
||||
func (d *promptDir) Stat() (fs.FileInfo, error) { return promptFileInfo{name: d.name, dir: true}, nil }
|
||||
func (d *promptDir) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
func (d *promptDir) Close() error { return nil }
|
||||
func (d *promptDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
if d.offset >= len(d.entries) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
end := len(d.entries)
|
||||
if n > 0 && d.offset+n < end {
|
||||
end = d.offset + n
|
||||
}
|
||||
out := append([]fs.DirEntry(nil), d.entries[d.offset:end]...)
|
||||
d.offset = end
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type promptDirEntry struct {
|
||||
name string
|
||||
dir bool
|
||||
size int64
|
||||
}
|
||||
|
||||
func (e promptDirEntry) Name() string { return e.name }
|
||||
func (e promptDirEntry) IsDir() bool { return e.dir }
|
||||
func (e promptDirEntry) Type() fs.FileMode { return e.fileInfoMode().Type() }
|
||||
func (e promptDirEntry) Info() (fs.FileInfo, error) {
|
||||
return promptFileInfo{name: e.name, dir: e.dir, size: e.size}, nil
|
||||
}
|
||||
func (e promptDirEntry) fileInfoMode() fs.FileMode {
|
||||
if e.dir {
|
||||
return fs.ModeDir | 0o555
|
||||
}
|
||||
return 0o444
|
||||
}
|
||||
|
||||
type promptFileInfo struct {
|
||||
name string
|
||||
dir bool
|
||||
size int64
|
||||
}
|
||||
|
||||
func (i promptFileInfo) Name() string { return i.name }
|
||||
func (i promptFileInfo) Size() int64 { return i.size }
|
||||
func (i promptFileInfo) Mode() fs.FileMode { return promptDirEntry{dir: i.dir}.fileInfoMode() }
|
||||
func (i promptFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (i promptFileInfo) IsDir() bool { return i.dir }
|
||||
func (i promptFileInfo) Sys() any { return nil }
|
||||
|
||||
@@ -103,6 +103,70 @@ func TestModulePromptFSRejectsNestedSharedFileName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsDuplicateNormalizedDestinations(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
call func() error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "module",
|
||||
call: func() error {
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/first.md": {Data: []byte("first")},
|
||||
"assets/prompts/second.md": {Data: []byte("second")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/first.md"},
|
||||
{Name: " task.md ", Path: "assets/prompts/second.md"},
|
||||
})
|
||||
return err
|
||||
},
|
||||
want: `duplicate module prompt destination "assets/prompts/module.test/task.md"`,
|
||||
},
|
||||
{
|
||||
name: "sharedassets",
|
||||
call: func() error {
|
||||
shared := fstest.MapFS{
|
||||
"shared/first.md": {Data: []byte("first")},
|
||||
"shared/second.md": {Data: []byte("second")},
|
||||
}
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{}, nil,
|
||||
SharedPromptFile{Name: "task.md", FS: shared, Path: "shared/first.md"},
|
||||
SharedPromptFile{Name: " task.md ", FS: shared, Path: "shared/second.md"},
|
||||
)
|
||||
return err
|
||||
},
|
||||
want: `duplicate sharedassets prompt destination "assets/prompts/module.test/sharedassets/task.md"`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := test.call(); err == nil || err.Error() != test.want {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSAllowsMatchingModuleAndSharedBasenames(t *testing.T) {
|
||||
fsys, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/system.md": {Data: []byte("module")},
|
||||
}, []ModulePromptFile{{Name: "system.md", Path: "assets/prompts/system.md"}},
|
||||
SharedPromptFile{Name: "system.md", FS: fstest.MapFS{"shared/system.md": {Data: []byte("shared")}}, Path: "shared/system.md"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, want := range map[string]string{
|
||||
"assets/prompts/module.test/system.md": "module",
|
||||
"assets/prompts/module.test/sharedassets/system.md": "shared",
|
||||
} {
|
||||
data, err := fs.ReadFile(fsys, name)
|
||||
if err != nil || string(data) != want {
|
||||
t.Fatalf("ReadFile(%q) = %q, %v; want %q", name, data, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsInvalidModuleDir(t *testing.T) {
|
||||
_, err := ModulePromptFS(".", fstest.MapFS{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "module prompt directory") {
|
||||
|
||||
Reference in New Issue
Block a user