add cli tool.
This commit is contained in:
39
cli/README.md
Normal file
39
cli/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# eventstore command-line tool
|
||||
|
||||
```
|
||||
go install github.com/fiatjaf/eventstore/cli@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This should be pretty straightforward. You pipe events or filters, as JSON, to the `eventstore` command, and they yield something. You can use [nak](https://github.com/fiatjaf/nak) to generate these events or filters easily.
|
||||
|
||||
### Querying the last 100 events of kind 1
|
||||
|
||||
```fish
|
||||
~> nak req -k 1 -l 100 --bare | eventstore -d /path/to/store query
|
||||
~> # or
|
||||
~> echo '{"kinds":[1],"limit":100}' | eventstore -d /path/to/store query
|
||||
```
|
||||
|
||||
This will automatically determine the storage type being used at `/path/to/store`, but you can also specify it manually using the `-t` option (`-t lmdb`, `-t sqlite` etc).
|
||||
|
||||
### Saving an event to the store
|
||||
|
||||
```fish
|
||||
~> nak event -k 1 -c hello | eventstore -d /path/to/store put
|
||||
~> # or
|
||||
~> echo '{"id":"35369e6bae5f77c4e1745c2eb5db84c4493e87f6e449aee62a261bbc1fea2788","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1701193836,"kind":1,"tags":[],"content":"hello","sig":"ef08d559e042d9af4cdc3328a064f737603d86ec4f929f193d5a3ce9ea22a3fb8afc1923ee3c3742fd01856065352c5632e91f633528c80e9c5711fa1266824c"}' | eventstore -d /path/to/store put
|
||||
```
|
||||
|
||||
You can also create a database from scratch if it's a disk database, but then you have to specify `-t` to `sqlite`, `badger` or `lmdb`.
|
||||
|
||||
### Connecting to Postgres, MySQL and other remote databases
|
||||
|
||||
You should be able to connect by just passing the database connection URI to `-d`:
|
||||
|
||||
```bash
|
||||
~> eventstore -d 'postgres://myrelay:38yg4o83yf48a3s7g@localhost:5432/myrelay?sslmode=disable' <query|put|del>
|
||||
```
|
||||
|
||||
That should be prefixed with `postgres://` for Postgres, `mysql://` for MySQL and `https://` for ElasticSearch.
|
||||
21
cli/del.go
Normal file
21
cli/del.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var del = &cli.Command{
|
||||
Name: "del",
|
||||
Usage: "deletes an event",
|
||||
Description: ``,
|
||||
Action: func(c *cli.Context) error {
|
||||
for line := range getStdinLinesOrBlank() {
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
109
cli/helpers.go
Normal file
109
cli/helpers.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
LINE_PROCESSING_ERROR = iota
|
||||
)
|
||||
|
||||
func detect(dir string) (string, error) {
|
||||
f, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !f.IsDir() {
|
||||
f, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf := make([]byte, 15)
|
||||
f.Read(buf)
|
||||
if string(buf) == "SQLite format 3" {
|
||||
return "sqlite", nil
|
||||
}
|
||||
return "", fmt.Errorf("unknown format")
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".mdb") {
|
||||
return "lmdb", nil
|
||||
}
|
||||
if strings.HasSuffix(entry.Name(), ".vlog") {
|
||||
return "badger", nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("undetected")
|
||||
}
|
||||
|
||||
func getStdin() string {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
read := bytes.NewBuffer(make([]byte, 0, 1000))
|
||||
_, err := io.Copy(read, os.Stdin)
|
||||
if err == nil {
|
||||
return read.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isPiped() bool {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
return stat.Mode()&os.ModeCharDevice == 0
|
||||
}
|
||||
|
||||
func getStdinLinesOrBlank() chan string {
|
||||
multi := make(chan string)
|
||||
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
|
||||
single := make(chan string, 1)
|
||||
single <- ""
|
||||
close(single)
|
||||
return single
|
||||
} else {
|
||||
return multi
|
||||
}
|
||||
}
|
||||
|
||||
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
||||
if isPiped() {
|
||||
// piped
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
ch <- strings.TrimSpace(scanner.Text())
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return true
|
||||
} else {
|
||||
// not piped
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func lineProcessingError(c *cli.Context, msg string, args ...any) {
|
||||
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
|
||||
fmt.Fprintf(os.Stderr, msg, args...)
|
||||
}
|
||||
|
||||
func exitIfLineProcessingError(c *cli.Context) {
|
||||
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
|
||||
os.Exit(123)
|
||||
}
|
||||
}
|
||||
99
cli/main.go
Normal file
99
cli/main.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/eventstore"
|
||||
"github.com/fiatjaf/eventstore/badger"
|
||||
"github.com/fiatjaf/eventstore/elasticsearch"
|
||||
"github.com/fiatjaf/eventstore/lmdb"
|
||||
"github.com/fiatjaf/eventstore/mysql"
|
||||
"github.com/fiatjaf/eventstore/postgresql"
|
||||
"github.com/fiatjaf/eventstore/sqlite3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var db eventstore.Store
|
||||
|
||||
var app = &cli.App{
|
||||
Name: "eventstore",
|
||||
Usage: "a CLI for all the eventstore backends",
|
||||
UsageText: "eventstore -d ./data/sqlite <query|put|del> ...",
|
||||
Flags: []cli.Flag{
|
||||
&cli.PathFlag{
|
||||
Name: "store",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "path to the database file or directory or database connection uri",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "store type ('sqlite', 'lmdb', 'badger', 'postgres', 'mysql', 'elasticsearch')",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
path := c.Path("store")
|
||||
typ := c.String("type")
|
||||
if typ != "" {
|
||||
// bypass automatic detection
|
||||
// this also works for creating disk databases from scratch
|
||||
} else {
|
||||
// try to detect based on url scheme
|
||||
switch {
|
||||
case strings.HasPrefix(path, "postgres://"), strings.HasPrefix(path, "postgresql://"):
|
||||
typ = "postgres"
|
||||
case strings.HasPrefix(path, "mysql://"):
|
||||
typ = "mysql"
|
||||
case strings.HasPrefix(path, "https://"):
|
||||
// if we ever add something else that uses URLs we'll have to modify this
|
||||
typ = "elasticsearch"
|
||||
default:
|
||||
// try to detect based on the form and names of disk files
|
||||
dbname, err := detect(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf(
|
||||
"'%s' does not exist, to create a store there specify the --type argument", path)
|
||||
}
|
||||
return fmt.Errorf("failed to detect store type: %w", err)
|
||||
}
|
||||
typ = dbname
|
||||
}
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "sqlite":
|
||||
db = &sqlite3.SQLite3Backend{DatabaseURL: path}
|
||||
case "lmdb":
|
||||
db = &lmdb.LMDBBackend{Path: path, MaxLimit: 5000}
|
||||
case "badger":
|
||||
db = &badger.BadgerBackend{Path: path, MaxLimit: 5000}
|
||||
case "postgres", "postgresql":
|
||||
db = &postgresql.PostgresBackend{DatabaseURL: path}
|
||||
case "mysql":
|
||||
db = &mysql.MySQLBackend{DatabaseURL: path}
|
||||
case "elasticsearch":
|
||||
db = &elasticsearch.ElasticsearchStorage{URL: path}
|
||||
case "":
|
||||
return fmt.Errorf("couldn't determine store type, you can use --type to specify it manually")
|
||||
default:
|
||||
return fmt.Errorf("'%s' store type is not supported by this CLI", typ)
|
||||
}
|
||||
|
||||
return db.Init()
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
query,
|
||||
put,
|
||||
del,
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
30
cli/put.go
Normal file
30
cli/put.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var put = &cli.Command{
|
||||
Name: "put",
|
||||
Usage: "saves an event to an eventstore",
|
||||
Description: ``,
|
||||
Action: func(c *cli.Context) error {
|
||||
for line := range getStdinLinesOrBlank() {
|
||||
var event nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(line), &event); err != nil {
|
||||
lineProcessingError(c, "invalid event '%s' received from stdin: %s", line, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := db.SaveEvent(c.Context, &event); err != nil {
|
||||
lineProcessingError(c, "failed to save event '%s': %s", line, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
37
cli/query.go
Normal file
37
cli/query.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var query = &cli.Command{
|
||||
Name: "query",
|
||||
Usage: "queries an eventstore for events",
|
||||
Description: ``,
|
||||
Action: func(c *cli.Context) error {
|
||||
for line := range getStdinLinesOrBlank() {
|
||||
filter := nostr.Filter{}
|
||||
if err := easyjson.Unmarshal([]byte(line), &filter); err != nil {
|
||||
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", line, err)
|
||||
continue
|
||||
}
|
||||
|
||||
ch, err := db.QueryEvents(c.Context, filter)
|
||||
if err != nil {
|
||||
lineProcessingError(c, "error querying: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for evt := range ch {
|
||||
fmt.Println(evt)
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
4
go.mod
4
go.mod
@@ -13,12 +13,14 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
github.com/nbd-wtf/go-nostr v0.25.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
@@ -43,9 +45,11 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -14,6 +14,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -123,6 +125,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -139,6 +143,10 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
|
||||
Reference in New Issue
Block a user