From 92588ac842015916a218cb225d7c0ae51b9d8827 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 19 Jul 2024 22:16:50 -0300 Subject: [PATCH] add strfry backend. --- cmd/eventstore/main.go | 5 ++ strfry/lib.go | 160 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 strfry/lib.go diff --git a/cmd/eventstore/main.go b/cmd/eventstore/main.go index 90928ff..73d9a4e 100644 --- a/cmd/eventstore/main.go +++ b/cmd/eventstore/main.go @@ -14,6 +14,7 @@ import ( "github.com/fiatjaf/eventstore/mysql" "github.com/fiatjaf/eventstore/postgresql" "github.com/fiatjaf/eventstore/sqlite3" + "github.com/fiatjaf/eventstore/strfry" "github.com/urfave/cli/v3" ) @@ -52,6 +53,8 @@ var app = &cli.Command{ case strings.HasPrefix(path, "https://"): // if we ever add something else that uses URLs we'll have to modify this typ = "elasticsearch" + case strings.HasSuffix(path, ".conf"): + typ = "strfry" default: // try to detect based on the form and names of disk files dbname, err := detect(path) @@ -102,6 +105,8 @@ var app = &cli.Command{ } case "elasticsearch": db = &elasticsearch.ElasticsearchStorage{URL: path} + case "strfry": + db = &strfry.StrfryBackend{ConfigPath: path} case "": return fmt.Errorf("couldn't determine store type, you can use --type to specify it manually") default: diff --git a/strfry/lib.go b/strfry/lib.go new file mode 100644 index 0000000..6e8ab37 --- /dev/null +++ b/strfry/lib.go @@ -0,0 +1,160 @@ +package strfry + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/fiatjaf/eventstore" + "github.com/mailru/easyjson" + "github.com/nbd-wtf/go-nostr" +) + +var _ eventstore.Store = (*StrfryBackend)(nil) + +type StrfryBackend struct { + ConfigPath string + ExecutablePath string +} + +func (s *StrfryBackend) Init() error { + if s.ExecutablePath == "" { + configPath := filepath.Dir(s.ConfigPath) + os.Setenv("PATH", configPath+":"+os.Getenv("PATH")) + exe, err := exec.LookPath("strfry") + if err != nil { + return fmt.Errorf("failed to find strfry executable: %w (better provide it manually)", err) + } + s.ExecutablePath = exe + } + + return nil +} + +func (_ StrfryBackend) Close() {} + +func (s StrfryBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) { + stdout, err := s.baseStrfryScan(ctx, filter) + if err != nil { + return nil, err + } + + ch := make(chan *nostr.Event) + go func() { + defer close(ch) + for { + line, err := stdout.ReadBytes('\n') + if err != nil { + break + } + + evt := &nostr.Event{} + easyjson.Unmarshal(line, evt) + if evt.ID == "" { + continue + } + + ch <- evt + } + }() + + return ch, nil +} + +func (s StrfryBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error { + args := make([]string, 0, 4) + if s.ConfigPath != "" { + args = append(args, "--config="+s.ConfigPath) + } + args = append(args, "import") + args = append(args, "--show-rejected") + args = append(args, "--no-verify") + + cmd := exec.CommandContext(ctx, s.ExecutablePath, args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + // event is sent on stdin + j, _ := easyjson.Marshal(evt) + cmd.Stdin = bytes.NewBuffer(j) + + err := cmd.Run() + if err != nil { + return fmt.Errorf( + "%s %s failed: %w, (%s)", + s.ExecutablePath, strings.Join(args, " "), err, stderr.String(), + ) + } + + return nil +} + +func (s StrfryBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error { + args := make([]string, 0, 3) + if s.ConfigPath != "" { + args = append(args, "--config="+s.ConfigPath) + } + args = append(args, "delete") + args = append(args, "--filter={\"ids\":[\""+evt.ID+"\"]}") + + cmd := exec.CommandContext(ctx, s.ExecutablePath, args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return fmt.Errorf( + "%s %s failed: %w, (%s)", + s.ExecutablePath, strings.Join(args, " "), err, stderr.String(), + ) + } + + return nil +} + +func (s StrfryBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) { + stdout, err := s.baseStrfryScan(ctx, filter) + if err != nil { + return 0, err + } + + var count int64 + for { + _, err := stdout.ReadBytes('\n') + if err != nil { + break + } + count++ + } + + return count, nil +} + +func (s StrfryBackend) baseStrfryScan(ctx context.Context, filter nostr.Filter) (*bytes.Buffer, error) { + args := make([]string, 0, 3) + if s.ConfigPath != "" { + args = append(args, "--config="+s.ConfigPath) + } + args = append(args, "scan") + args = append(args, filter.String()) + + cmd := exec.CommandContext(ctx, s.ExecutablePath, args...) + var stdout bytes.Buffer + cmd.Stdout = &stdout + var stderr bytes.Buffer + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf( + "%s %s failed: %w, (%s)", + s.ExecutablePath, strings.Join(args, " "), err, stderr.String(), + ) + } + + return &stdout, nil +}