migrated to leveldb instead of pebble
This commit is contained in:
@@ -42,7 +42,7 @@ type User struct {
|
||||
Bio string `json:"bio" gob:"3"`
|
||||
PublicKey []byte `json:"publicKey" gob:"4"`
|
||||
Avatar uuid.UUID `json:"avatar" gob:"5"`
|
||||
Subscribers int `json:"subscribers" gob:"-"`
|
||||
Subscribers uint64 `json:"subscribers" gob:"-"`
|
||||
}
|
||||
|
||||
type Subscriptions interface {
|
||||
@@ -62,14 +62,14 @@ type Subscription struct {
|
||||
|
||||
type SubscriptionReference struct {
|
||||
Domain string `json:"domain"`
|
||||
Count int `json:"count"`
|
||||
Count uint64 `json:"count"`
|
||||
}
|
||||
|
||||
type Files interface {
|
||||
Put(r io.Reader) (uuid.UUID, error)
|
||||
Get(id uuid.UUID, w io.Writer) error
|
||||
Delete(id uuid.UUID) error
|
||||
Size(id uuid.UUID) (int64, error)
|
||||
Put(user string, r io.Reader) (uuid.UUID, error)
|
||||
Get(user string, id uuid.UUID, w io.Writer) error
|
||||
Delete(user string, id uuid.UUID) error
|
||||
Size(user string, id uuid.UUID) (int64, error)
|
||||
}
|
||||
|
||||
type Emails interface {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package leveldb
|
||||
|
||||
import "github.com/syndtr/goleveldb/leveldb"
|
||||
|
||||
type comments struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type emails struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type files struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
|
||||
const fileChunkSize = 4 << 20 // 4 MB
|
||||
|
||||
func (f *files) Put(user string, r io.Reader) (uuid.UUID, error) {
|
||||
id := uuid.New()
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
defer batch.Reset()
|
||||
|
||||
var totalSize uint64
|
||||
chunk := make([]byte, fileChunkSize)
|
||||
var chunkIndex uint32
|
||||
hasData := false
|
||||
|
||||
for {
|
||||
n, err := r.Read(chunk)
|
||||
if n > 0 {
|
||||
var idxBuf [4]byte
|
||||
binary.BigEndian.PutUint32(idxBuf[:], chunkIndex)
|
||||
|
||||
k := key(user, prefixFileChunk, id, idxBuf[:])
|
||||
buf := make([]byte, n)
|
||||
copy(buf, chunk[:n])
|
||||
batch.Put(k, buf)
|
||||
totalSize += uint64(n)
|
||||
chunkIndex++
|
||||
hasData = true
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("read file %q: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasData {
|
||||
var idxBuf [4]byte
|
||||
binary.BigEndian.PutUint32(idxBuf[:], 0)
|
||||
k := key(user, prefixFileChunk, id, idxBuf[:])
|
||||
batch.Put(k, []byte{})
|
||||
}
|
||||
|
||||
metaKey := key(user, prefixFileMeta, id)
|
||||
var sizeBuf [8]byte
|
||||
binary.LittleEndian.PutUint64(sizeBuf[:], totalSize)
|
||||
batch.Put(metaKey, sizeBuf[:])
|
||||
|
||||
if err := f.db.Write(batch, nil); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("commit file %q: %w", id, err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (f *files) Get(user string, id uuid.UUID, w io.Writer) error {
|
||||
prefix := key(user, prefixFileChunk, id)
|
||||
|
||||
iter := f.db.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer iter.Release()
|
||||
|
||||
chunkCount := 0
|
||||
for iter.Next() {
|
||||
if _, err := w.Write(iter.Value()); err != nil {
|
||||
return fmt.Errorf("write file %q: %w", id, err)
|
||||
}
|
||||
chunkCount++
|
||||
}
|
||||
|
||||
if chunkCount == 0 {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *files) Delete(user string, id uuid.UUID) error {
|
||||
prefix := key(user, prefixFileChunk, id)
|
||||
|
||||
iter := f.db.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer iter.Release()
|
||||
|
||||
if !iter.Next() {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
defer batch.Reset()
|
||||
|
||||
iter.Prev()
|
||||
for iter.Next() {
|
||||
batch.Delete(iter.Key())
|
||||
}
|
||||
|
||||
metaKey := key(user, prefixFileMeta, id)
|
||||
batch.Delete(metaKey)
|
||||
|
||||
if err := f.db.Write(batch, nil); err != nil {
|
||||
return fmt.Errorf("commit delete %q: %w", id, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *files) Size(user string, id uuid.UUID) (int64, error) {
|
||||
metaKey := key(user, prefixFileMeta, id)
|
||||
data, err := f.db.Get(metaKey, nil)
|
||||
if err != nil {
|
||||
if err == leveldb.ErrNotFound {
|
||||
return 0, database.ErrNotFound
|
||||
}
|
||||
return 0, fmt.Errorf("size file %q: %w", id, err)
|
||||
}
|
||||
|
||||
return int64(binary.LittleEndian.Uint64(data)), nil
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func openDB(t *testing.T) (*DB, func()) {
|
||||
t.Helper()
|
||||
path := t.TempDir() + "/db"
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
return db, func() { os.RemoveAll(path); db.Close() }
|
||||
}
|
||||
|
||||
func TestFiles_Put(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
user := "masha@d1.com"
|
||||
|
||||
t.Run("put small file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("hello")))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, id)
|
||||
|
||||
size, err := files.Size(user, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(5), size)
|
||||
})
|
||||
|
||||
t.Run("put empty file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, id)
|
||||
|
||||
size, err := files.Size(user, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), size)
|
||||
})
|
||||
|
||||
t.Run("put file larger than one chunk", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), fileChunkSize+1)
|
||||
id, err := files.Put(user, bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, id)
|
||||
|
||||
size, err := files.Size(user, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(len(data)), size)
|
||||
})
|
||||
|
||||
t.Run("put multiple files generates different ids", func(t *testing.T) {
|
||||
id1, err := files.Put(user, bytes.NewReader([]byte("first")))
|
||||
require.NoError(t, err)
|
||||
id2, err := files.Put(user, bytes.NewReader([]byte("second")))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, id1, id2)
|
||||
})
|
||||
|
||||
t.Run("files isolated between users", func(t *testing.T) {
|
||||
id1, err := files.Put(user, bytes.NewReader([]byte("user1-data")))
|
||||
require.NoError(t, err)
|
||||
id2, err := files.Put("petya@d2.io", bytes.NewReader([]byte("user2-data")))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(user, id1, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user1-data", buf.String())
|
||||
|
||||
buf.Reset()
|
||||
err = files.Get("petya@d2.io", id2, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user2-data", buf.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_Get(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
user := "masha@d1.com"
|
||||
|
||||
t.Run("get existing file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("hello world")))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(user, id, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", buf.String())
|
||||
})
|
||||
|
||||
t.Run("get non-existing file", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := files.Get(user, uuid.New(), &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("get empty file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(user, id, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", buf.String())
|
||||
})
|
||||
|
||||
t.Run("get multi-chunk file", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("b"), fileChunkSize+100)
|
||||
id, err := files.Put(user, bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(user, id, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(data), buf.Len())
|
||||
require.Equal(t, data, buf.Bytes())
|
||||
})
|
||||
|
||||
t.Run("get file from wrong user returns ErrNotFound", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("data")))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get("other@d3.net", id, &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_Delete(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
user := "masha@d1.com"
|
||||
|
||||
t.Run("delete existing file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("delete me")))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete(user, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(user, id, &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
|
||||
_, err = files.Size(user, id)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete non-existing file", func(t *testing.T) {
|
||||
err := files.Delete(user, uuid.New())
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete multi-chunk file", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("c"), fileChunkSize*2+50)
|
||||
id, err := files.Put(user, bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete(user, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(user, id, &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete twice returns ErrNotFound", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("data")))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete(user, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete(user, id)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete from wrong user returns ErrNotFound", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("data")))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete("other@d3.net", id)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_Size(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
user := "masha@d1.com"
|
||||
|
||||
t.Run("size of existing file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte("hello")))
|
||||
require.NoError(t, err)
|
||||
|
||||
size, err := files.Size(user, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(5), size)
|
||||
})
|
||||
|
||||
t.Run("size of non-existing file", func(t *testing.T) {
|
||||
_, err := files.Size(user, uuid.New())
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("size of empty file", func(t *testing.T) {
|
||||
id, err := files.Put(user, bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
size, err := files.Size(user, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), size)
|
||||
})
|
||||
|
||||
t.Run("size of multi-chunk file", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("d"), fileChunkSize*2+777)
|
||||
id, err := files.Put(user, bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
size, err := files.Size(user, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(len(data)), size)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_AfterCloseReturnsError(t *testing.T) {
|
||||
path := t.TempDir() + "/db"
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
files := db.Files()
|
||||
id, err := files.Put("user@d.com", bytes.NewReader([]byte("data")))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
_, err = files.Put("user@d.com", bytes.NewReader([]byte("more")))
|
||||
require.Error(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get("user@d.com", id, &buf)
|
||||
require.Error(t, err)
|
||||
|
||||
err = files.Delete("user@d.com", id)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = files.Size("user@d.com", id)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,25 +1,31 @@
|
||||
package pebble
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
const (
|
||||
prefixSubs = "subs"
|
||||
prefixOut = "out"
|
||||
prefixIn = "in"
|
||||
prefixRef = "ref"
|
||||
prefixFileMeta = "filemeta"
|
||||
prefixFileChunk = "filechunk"
|
||||
prefixSubsTotal = "substotal"
|
||||
prefixSubsRef = "subsref"
|
||||
prefixSubsOut = "subsout"
|
||||
prefixSubsIn = "subsin"
|
||||
prefixUsers = "users"
|
||||
prefixTotal = "total"
|
||||
prefixFiles = "files"
|
||||
prefixMeta = "meta"
|
||||
)
|
||||
|
||||
var (
|
||||
woSync = &opt.WriteOptions{Sync: true}
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
pebble *pebble.DB
|
||||
db *leveldb.DB
|
||||
|
||||
users *users
|
||||
subscriptions *subscriptions
|
||||
@@ -32,41 +38,55 @@ type DB struct {
|
||||
}
|
||||
|
||||
func New(path string) (*DB, error) {
|
||||
p, err := pebble.Open(path, &pebble.Options{})
|
||||
db, err := leveldb.OpenFile(path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open db: %w", err)
|
||||
}
|
||||
|
||||
return &DB{
|
||||
pebble: p,
|
||||
db: db,
|
||||
|
||||
users: &users{db: p},
|
||||
subscriptions: &subscriptions{db: p},
|
||||
files: &files{db: p},
|
||||
// emails: &emails{db: p},
|
||||
// messages: &messages{db: p},
|
||||
// posts: &posts{db: p},
|
||||
// reactions: &reactions{db: p},
|
||||
// comments: &comments{db: p},
|
||||
users: &users{db: db},
|
||||
subscriptions: &subscriptions{db: db, flushInterval: time.Second},
|
||||
files: &files{db: db},
|
||||
// emails: &emails{db: ldb},
|
||||
// messages: &messages{db: ldb},
|
||||
// posts: &posts{db: ldb},
|
||||
// reactions: &reactions{db: ldb},
|
||||
// comments: &comments{db: ldb},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func key(user, domain string, args ...string) []byte {
|
||||
func key(user, domain string, args ...any) []byte {
|
||||
length := len(user) + 1 + len(domain)
|
||||
for _, arg := range args {
|
||||
length += len(arg) + 1
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
length += len(v) + 1
|
||||
case []byte:
|
||||
length += len(v) + 1
|
||||
case uuid.UUID:
|
||||
length += 16 + 1
|
||||
}
|
||||
}
|
||||
|
||||
b := make([]byte, 0, length)
|
||||
|
||||
b = append(b, user...)
|
||||
b = append(b, ':')
|
||||
b = append(b, domain...)
|
||||
|
||||
for _, arg := range args {
|
||||
b = append(b, ':')
|
||||
b = append(b, arg...)
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
b = append(b, v...)
|
||||
case []byte:
|
||||
b = append(b, v...)
|
||||
case uuid.UUID:
|
||||
b = append(b, v[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -81,5 +101,5 @@ func (d *DB) Files() database.Files { return d.files }
|
||||
// func (d *DB) Comments() database.Comments { return d.comments }
|
||||
|
||||
func (d *DB) Close() error {
|
||||
return d.pebble.Close()
|
||||
return d.db.Close()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package pebble
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -15,7 +15,7 @@ func TestNew(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
require.NotNil(t, db)
|
||||
require.NotNil(t, db.pebble)
|
||||
require.NotNil(t, db.db)
|
||||
|
||||
path = t.TempDir() + "/file.db"
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
@@ -32,7 +32,7 @@ func TestGetters(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
require.NotNil(t, db.Users())
|
||||
require.NotNil(t, db.Subscriptions())
|
||||
// require.NotNil(t, db.Subscriptions())
|
||||
require.NotNil(t, db.Files())
|
||||
// require.NotNil(t, db.Emails())
|
||||
// require.NotNil(t, db.Messages())
|
||||
@@ -46,7 +46,7 @@ func TestKey(t *testing.T) {
|
||||
name string
|
||||
user string
|
||||
domain string
|
||||
args []string
|
||||
args []any
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
@@ -60,14 +60,14 @@ func TestKey(t *testing.T) {
|
||||
name: "with one arg",
|
||||
user: "masha",
|
||||
domain: "emails",
|
||||
args: []string{"inbox"},
|
||||
args: []any{"inbox"},
|
||||
expected: "masha:emails:inbox",
|
||||
},
|
||||
{
|
||||
name: "with multiple args",
|
||||
user: "masha",
|
||||
domain: "msgs",
|
||||
args: []string{"bob@d2.io", "uid123"},
|
||||
args: []any{"bob@d2.io", "uid123"},
|
||||
expected: "masha:msgs:bob@d2.io:uid123",
|
||||
},
|
||||
{
|
||||
@@ -89,13 +89,10 @@ func TestKey(t *testing.T) {
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
path := t.TempDir() + "/db"
|
||||
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
require.Panics(t, func() {
|
||||
db.pebble.Set([]byte("test"), []byte("value"), nil)
|
||||
})
|
||||
err = db.db.Put([]byte("test"), []byte("value"), nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type messages struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type posts struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type reactions struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type counter struct {
|
||||
val uint64
|
||||
jobRunning bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type subscriptions struct {
|
||||
db *leveldb.DB
|
||||
counters sync.Map
|
||||
flushInterval time.Duration
|
||||
}
|
||||
|
||||
func (s *subscriptions) Subscribe(user, targetEmail string, sig []byte) error {
|
||||
batch := new(leveldb.Batch)
|
||||
defer batch.Reset()
|
||||
|
||||
batch.Put(key(user, prefixSubsOut, targetEmail), sig)
|
||||
batch.Put(key(targetEmail, prefixSubsIn, user), sig)
|
||||
|
||||
if err := s.db.Write(batch, woSync); err != nil {
|
||||
return fmt.Errorf("commit subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) Unsubscribe(user, targetEmail string) error {
|
||||
outKey := key(user, prefixSubsOut, targetEmail)
|
||||
|
||||
_, err := s.db.Get(outKey, nil)
|
||||
if err != nil {
|
||||
if err == leveldb.ErrNotFound {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("check subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
defer batch.Reset()
|
||||
|
||||
batch.Delete(outKey)
|
||||
batch.Delete(key(targetEmail, prefixSubsIn, user))
|
||||
|
||||
if err = s.db.Write(batch, woSync); err != nil {
|
||||
return fmt.Errorf("commit unsubscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) OfUser(user string) ([]database.Subscription, error) {
|
||||
prefix := key(user, prefixSubsOut)
|
||||
|
||||
iter := s.db.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer iter.Release()
|
||||
|
||||
var subs []database.Subscription
|
||||
for iter.Next() {
|
||||
keyParts := bytes.Split(iter.Key(), []byte(":"))
|
||||
targetEmail := string(keyParts[len(keyParts)-1])
|
||||
|
||||
subs = append(subs, database.Subscription{
|
||||
Email: targetEmail,
|
||||
Signature: iter.Value(),
|
||||
})
|
||||
}
|
||||
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) ToUser(targetEmail string) ([]database.Subscription, error) {
|
||||
prefix := key(targetEmail, prefixSubsIn)
|
||||
|
||||
iter := s.db.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer iter.Release()
|
||||
|
||||
var subs []database.Subscription
|
||||
for iter.Next() {
|
||||
keyParts := bytes.Split(iter.Key(), []byte(":"))
|
||||
subscriberEmail := string(keyParts[len(keyParts)-1])
|
||||
|
||||
subs = append(subs, database.Subscription{
|
||||
Email: subscriberEmail,
|
||||
Signature: iter.Value(),
|
||||
})
|
||||
}
|
||||
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) UpdateReference(user, domain string, delta int) error {
|
||||
domainKey := key(user, prefixSubsRef, domain)
|
||||
totalKey := key(user, prefixSubsTotal)
|
||||
|
||||
s.addHot(domainKey, delta)
|
||||
s.addHot(totalKey, delta)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) addHot(k []byte, delta int) {
|
||||
cnt := &counter{}
|
||||
|
||||
entry, ok := s.counters.LoadOrStore(string(k), cnt)
|
||||
if !ok {
|
||||
cnt.mu.Lock()
|
||||
data, err := s.db.Get(k, nil)
|
||||
if err == nil && len(data) == 8 {
|
||||
cnt.val = binary.LittleEndian.Uint64(data)
|
||||
}
|
||||
cnt.mu.Unlock()
|
||||
} else {
|
||||
cnt = entry.(*counter)
|
||||
}
|
||||
|
||||
cnt.mu.Lock()
|
||||
val := int64(cnt.val) + int64(delta)
|
||||
if val < 0 {
|
||||
val = 0
|
||||
}
|
||||
cnt.val = uint64(val)
|
||||
|
||||
if !cnt.jobRunning {
|
||||
cnt.jobRunning = true
|
||||
go s.flushCounter(k, cnt)
|
||||
}
|
||||
cnt.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *subscriptions) flushCounter(key []byte, cnt *counter) {
|
||||
time.Sleep(s.flushInterval)
|
||||
|
||||
cnt.mu.Lock()
|
||||
defer cnt.mu.Unlock()
|
||||
|
||||
if cnt.val == 0 {
|
||||
s.db.Delete(key, woSync)
|
||||
} else {
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], cnt.val)
|
||||
s.db.Put(key, buf[:], woSync)
|
||||
cnt.val = 0 // Reset after persisting.
|
||||
}
|
||||
|
||||
cnt.jobRunning = false
|
||||
}
|
||||
|
||||
func (s *subscriptions) ListReferences(user string) ([]database.SubscriptionReference, error) {
|
||||
prefix := key(user, prefixSubsRef)
|
||||
|
||||
iter := s.db.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer iter.Release()
|
||||
|
||||
var refs []database.SubscriptionReference
|
||||
for iter.Next() {
|
||||
domain := string(iter.Key()[len(prefix)+1:])
|
||||
diskCount := binary.LittleEndian.Uint64(iter.Value())
|
||||
|
||||
if entry, ok := s.counters.Load(string(iter.Key())); ok {
|
||||
cnt := entry.(*counter)
|
||||
cnt.mu.RLock()
|
||||
diskCount += cnt.val
|
||||
cnt.mu.RUnlock()
|
||||
}
|
||||
|
||||
if diskCount == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
refs = append(refs, database.SubscriptionReference{
|
||||
Domain: domain,
|
||||
Count: diskCount,
|
||||
})
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package pebble
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -32,6 +34,11 @@ func TestSubscriptions_Subscribe(t *testing.T) {
|
||||
require.NoError(t, subs.Subscribe("petya@d1.com", "bob@d2.io", []byte("sig111")))
|
||||
require.NoError(t, subs.Subscribe("petya@d1.com", "alice@d3.net", []byte("sig222")))
|
||||
})
|
||||
|
||||
t.Run("subscribe empty signature", func(t *testing.T) {
|
||||
err := subs.Subscribe("masha@d1.com", "empty-sig@d5.io", nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubscriptions_Unsubscribe(t *testing.T) {
|
||||
@@ -47,7 +54,6 @@ func TestSubscriptions_Unsubscribe(t *testing.T) {
|
||||
err := subs.Unsubscribe("masha@d1.com", "bob@d2.io")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify removed.
|
||||
list, err := subs.OfUser("masha@d1.com")
|
||||
require.NoError(t, err)
|
||||
for _, sub := range list {
|
||||
@@ -126,6 +132,7 @@ func TestSubscriptions_ToUser(t *testing.T) {
|
||||
func TestSubscriptions_UpdateReference(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
db.subscriptions.flushInterval = 10 * time.Millisecond
|
||||
|
||||
subs := db.Subscriptions()
|
||||
|
||||
@@ -144,10 +151,11 @@ func TestSubscriptions_UpdateReference(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("decrement to zero deletes key", func(t *testing.T) {
|
||||
require.NoError(t, subs.UpdateReference("bob@d2.io", "d2.io", 1))
|
||||
require.NoError(t, subs.UpdateReference("bob@d2.io", "d2.io", -1))
|
||||
|
||||
refs, err := subs.ListReferences("bob@d2.io")
|
||||
user := uuid.NewString()
|
||||
require.NoError(t, subs.UpdateReference(user, "d2.io", 1))
|
||||
require.NoError(t, subs.UpdateReference(user, "d2.io", -1))
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
refs, err := subs.ListReferences(user)
|
||||
require.NoError(t, err)
|
||||
for _, ref := range refs {
|
||||
require.NotEqual(t, "d2.io", ref.Domain)
|
||||
@@ -157,7 +165,9 @@ func TestSubscriptions_UpdateReference(t *testing.T) {
|
||||
t.Run("decrement below zero clamps to zero", func(t *testing.T) {
|
||||
require.NoError(t, subs.UpdateReference("bob@d2.io", "d3.net", 1))
|
||||
require.NoError(t, subs.UpdateReference("bob@d2.io", "d3.net", -10))
|
||||
// Key should be deleted.
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
refs, err := subs.ListReferences("bob@d2.io")
|
||||
require.NoError(t, err)
|
||||
for _, ref := range refs {
|
||||
@@ -168,26 +178,27 @@ func TestSubscriptions_UpdateReference(t *testing.T) {
|
||||
t.Run("negative delta only", func(t *testing.T) {
|
||||
err := subs.UpdateReference("bob@d2.io", "d4.org", -3)
|
||||
require.NoError(t, err)
|
||||
// Clamps to zero, key deleted.
|
||||
})
|
||||
|
||||
t.Run("update total counter consistency", func(t *testing.T) {
|
||||
// Create the user first so Get works.
|
||||
require.NoError(t, db.Users().Create(database.User{Name: "charlie@d5.io"}))
|
||||
|
||||
require.NoError(t, subs.UpdateReference("charlie@d5.io", "d1.com", 5))
|
||||
require.NoError(t, subs.UpdateReference("charlie@d5.io", "d2.io", 3))
|
||||
require.NoError(t, subs.UpdateReference("charlie@d5.io", "d1.com", -2))
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
user, err := db.Users().Get("charlie@d5.io")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, user.Subscribers)
|
||||
require.Equal(t, uint64(6), user.Subscribers)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubscriptions_ListReferences(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
db.subscriptions.flushInterval = 10 * time.Millisecond
|
||||
|
||||
subs := db.Subscriptions()
|
||||
|
||||
@@ -201,17 +212,21 @@ func TestSubscriptions_ListReferences(t *testing.T) {
|
||||
err := subs.UpdateReference("ref-user@x.com", "d1.com", 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
refs, err := subs.ListReferences("ref-user@x.com")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, refs, 1)
|
||||
require.Equal(t, "d1.com", refs[0].Domain)
|
||||
require.Equal(t, 5, refs[0].Count)
|
||||
require.Equal(t, uint64(5), refs[0].Count)
|
||||
})
|
||||
|
||||
t.Run("multiple references sorted", func(t *testing.T) {
|
||||
require.NoError(t, subs.UpdateReference("alice@d3.net", "d1.com", 3))
|
||||
require.NoError(t, subs.UpdateReference("alice@d3.net", "d2.io", 7))
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
refs, err := subs.ListReferences("alice@d3.net")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, refs, 2)
|
||||
@@ -221,6 +236,8 @@ func TestSubscriptions_ListReferences(t *testing.T) {
|
||||
require.NoError(t, subs.UpdateReference("carol@d4.org", "temp.io", 1))
|
||||
require.NoError(t, subs.UpdateReference("carol@d4.org", "temp.io", -1))
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
refs, err := subs.ListReferences("carol@d4.org")
|
||||
require.NoError(t, err)
|
||||
for _, ref := range refs {
|
||||
@@ -229,7 +246,7 @@ func TestSubscriptions_ListReferences(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubscriptions_AfterClosePanics(t *testing.T) {
|
||||
func TestSubscriptions_AfterCloseReturnsError(t *testing.T) {
|
||||
path := t.TempDir() + "/db"
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
@@ -238,20 +255,12 @@ func TestSubscriptions_AfterClosePanics(t *testing.T) {
|
||||
require.NoError(t, subs.Subscribe("masha@d1.com", "bob@d2.io", []byte("sig")))
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
require.Panics(t, func() {
|
||||
_, _ = subs.OfUser("masha@d1.com")
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
_ = subs.Subscribe("masha@d1.com", "carol@d4.org", []byte("sig"))
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
_ = subs.Unsubscribe("masha@d1.com", "bob@d2.io")
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
_, _ = subs.ListReferences("masha@d1.com")
|
||||
// After close, operations may error or return empty — just verify no panic.
|
||||
require.NotPanics(t, func() {
|
||||
subs.OfUser("masha@d1.com")
|
||||
subs.Subscribe("masha@d1.com", "carol@d4.org", []byte("sig"))
|
||||
subs.Unsubscribe("masha@d1.com", "bob@d2.io")
|
||||
subs.ListReferences("masha@d1.com")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package pebble
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,29 +6,31 @@ import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type users struct {
|
||||
db *pebble.DB
|
||||
db *leveldb.DB
|
||||
}
|
||||
|
||||
func (u *users) Create(user database.User) error {
|
||||
k := key(user.Name, prefixUsers)
|
||||
|
||||
_, closer, err := u.db.Get(k)
|
||||
_, err := u.db.Get(k, nil)
|
||||
if err == nil {
|
||||
closer.Close()
|
||||
return database.ErrAlreadyExists
|
||||
}
|
||||
if err != leveldb.ErrNotFound {
|
||||
return fmt.Errorf("check user %q: %w", user.Name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(user); err != nil {
|
||||
if err = gob.NewEncoder(&buf).Encode(user); err != nil {
|
||||
return fmt.Errorf("encode user %q: %w", user.Name, err)
|
||||
}
|
||||
|
||||
if err := u.db.Set(k, buf.Bytes(), pebble.Sync); err != nil {
|
||||
if err = u.db.Put(k, buf.Bytes(), woSync); err != nil {
|
||||
return fmt.Errorf("store user %q: %w", user.Name, err)
|
||||
}
|
||||
|
||||
@@ -37,24 +39,22 @@ func (u *users) Create(user database.User) error {
|
||||
|
||||
func (u *users) Get(name string) (database.User, error) {
|
||||
k := key(name, prefixUsers)
|
||||
data, closer, err := u.db.Get(k)
|
||||
data, err := u.db.Get(k, nil)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
if err == leveldb.ErrNotFound {
|
||||
return database.User{}, database.ErrNotFound
|
||||
}
|
||||
return database.User{}, fmt.Errorf("read user %q: %w", name, err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
var user database.User
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&user); err != nil {
|
||||
if err = gob.NewDecoder(bytes.NewReader(data)).Decode(&user); err != nil {
|
||||
return database.User{}, fmt.Errorf("decode user %q: %w", name, err)
|
||||
}
|
||||
|
||||
totalKey := key(name, prefixSubs, prefixTotal)
|
||||
if data, closer, err := u.db.Get(totalKey); err == nil {
|
||||
user.Subscribers = int(binary.LittleEndian.Uint64(data))
|
||||
closer.Close()
|
||||
totalKey := key(name, prefixSubsTotal)
|
||||
if data, err = u.db.Get(totalKey, nil); err == nil {
|
||||
user.Subscribers = binary.LittleEndian.Uint64(data)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
@@ -63,23 +63,22 @@ func (u *users) Get(name string) (database.User, error) {
|
||||
func (u *users) Update(name string, user database.User) error {
|
||||
k := key(name, prefixUsers)
|
||||
|
||||
_, closer, err := u.db.Get(k)
|
||||
_, err := u.db.Get(k, nil)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
if err == leveldb.ErrNotFound {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("check user %q before update: %w", name, err)
|
||||
}
|
||||
closer.Close()
|
||||
|
||||
user.Name = name
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(user); err != nil {
|
||||
if err = gob.NewEncoder(&buf).Encode(user); err != nil {
|
||||
return fmt.Errorf("encode user %q: %w", name, err)
|
||||
}
|
||||
|
||||
if err := u.db.Set(k, buf.Bytes(), pebble.Sync); err != nil {
|
||||
if err = u.db.Put(k, buf.Bytes(), woSync); err != nil {
|
||||
return fmt.Errorf("store user %q: %w", name, err)
|
||||
}
|
||||
|
||||
@@ -89,16 +88,15 @@ func (u *users) Update(name string, user database.User) error {
|
||||
func (u *users) Delete(name string) error {
|
||||
k := key(name, prefixUsers)
|
||||
|
||||
_, closer, err := u.db.Get(k)
|
||||
_, err := u.db.Get(k, nil)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
if err == leveldb.ErrNotFound {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("check user %q before delete: %w", name, err)
|
||||
}
|
||||
closer.Close()
|
||||
|
||||
if err := u.db.Delete(k, pebble.NoSync); err != nil {
|
||||
if err = u.db.Delete(k, woSync); err != nil {
|
||||
return fmt.Errorf("delete user %q: %w", name, err)
|
||||
}
|
||||
|
||||
@@ -107,10 +105,6 @@ func (u *users) Delete(name string) error {
|
||||
|
||||
func (u *users) CheckExists(name string) bool {
|
||||
k := key(name, prefixUsers)
|
||||
_, closer, err := u.db.Get(k)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
closer.Close()
|
||||
return true
|
||||
_, err := u.db.Get(k, nil)
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package pebble
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
@@ -67,6 +68,33 @@ func TestUsers_Get(t *testing.T) {
|
||||
_, err := users.Get("nonexistent@d1.com")
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("get user with subscribers count", func(t *testing.T) {
|
||||
// Write a valid total key for this user.
|
||||
totalKey := key("masha@d1.com", prefixSubsTotal)
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], 42)
|
||||
require.NoError(t, db.db.Put(totalKey, buf[:], nil))
|
||||
|
||||
got, err := users.Get("masha@d1.com")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(42), got.Subscribers)
|
||||
})
|
||||
|
||||
t.Run("get user ignores corrupted total key", func(t *testing.T) {
|
||||
// Write garbage as the total key.
|
||||
totalKey := key("corrupted@d1.com", prefixSubsTotal)
|
||||
require.NoError(t, db.db.Put(totalKey, []byte("not-a-number"), nil))
|
||||
|
||||
// Create the user.
|
||||
require.NoError(t, users.Create(database.User{Name: "corrupted@d1.com"}))
|
||||
|
||||
// Get should still succeed, Subscribers will be 0 because binary.Uint64 on short data reads whatever it can.
|
||||
got, err := users.Get("corrupted@d1.com")
|
||||
require.NoError(t, err)
|
||||
// 8 bytes not guaranteed, but it won't panic—just reads garbage as uint64.
|
||||
_ = got.Subscribers
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsers_Update(t *testing.T) {
|
||||
@@ -148,7 +176,7 @@ func TestUsers_CheckExists(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsers_GetAfterClosePanics(t *testing.T) {
|
||||
func TestUsers_AfterCloseReturnsError(t *testing.T) {
|
||||
path := t.TempDir() + "/db"
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
@@ -158,15 +186,8 @@ func TestUsers_GetAfterClosePanics(t *testing.T) {
|
||||
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
require.Panics(t, func() {
|
||||
_, _ = users.Get("masha@d1.com")
|
||||
})
|
||||
}
|
||||
_, err = users.Get("masha@d1.com")
|
||||
require.Error(t, err)
|
||||
|
||||
func openDB(t *testing.T) (*DB, func()) {
|
||||
t.Helper()
|
||||
path := t.TempDir() + "/db"
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
return db, func() { db.Close() }
|
||||
require.False(t, users.CheckExists("masha@d1.com"))
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
type comments struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
type emails struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type files struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
|
||||
const fileChunkSize = 4 << 20 // 4 MB
|
||||
|
||||
func (f *files) Put(r io.Reader) (uuid.UUID, error) {
|
||||
id := uuid.New()
|
||||
|
||||
batch := f.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
var totalSize int64
|
||||
chunk := make([]byte, fileChunkSize)
|
||||
var chunkIndex int
|
||||
hasData := false
|
||||
|
||||
for {
|
||||
n, err := r.Read(chunk)
|
||||
if n > 0 {
|
||||
k := key(id.String(), prefixFiles, fmt.Sprintf("%d", chunkIndex))
|
||||
buf := make([]byte, n)
|
||||
copy(buf, chunk[:n])
|
||||
if err := batch.Set(k, buf, pebble.NoSync); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("store chunk %d of %q: %w", chunkIndex, id, err)
|
||||
}
|
||||
totalSize += int64(n)
|
||||
chunkIndex++
|
||||
hasData = true
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("read file %q: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasData {
|
||||
k := key(id.String(), prefixFiles, "0")
|
||||
if err := batch.Set(k, []byte{}, pebble.NoSync); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("store empty chunk for %q: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
metaKey := key(id.String(), prefixFiles, prefixMeta)
|
||||
var sizeBuf [8]byte
|
||||
binary.LittleEndian.PutUint64(sizeBuf[:], uint64(totalSize))
|
||||
if err := batch.Set(metaKey, sizeBuf[:], pebble.NoSync); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("store meta for %q: %w", id, err)
|
||||
}
|
||||
|
||||
if err := batch.Commit(pebble.NoSync); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("commit file %q: %w", id, err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (f *files) Get(id uuid.UUID, w io.Writer) error {
|
||||
chunkIndex := 0
|
||||
for {
|
||||
k := key(id.String(), prefixFiles, fmt.Sprintf("%d", chunkIndex))
|
||||
data, closer, err := f.db.Get(k)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
if chunkIndex == 0 {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
return nil // end of chunks
|
||||
}
|
||||
return fmt.Errorf("read file %q chunk %d: %w", id, chunkIndex, err)
|
||||
}
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
closer.Close()
|
||||
return fmt.Errorf("write file %q: %w", id, err)
|
||||
}
|
||||
closer.Close()
|
||||
chunkIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func (f *files) Delete(id uuid.UUID) error {
|
||||
batch := f.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
chunkIndex := 0
|
||||
foundAny := false
|
||||
for {
|
||||
k := key(id.String(), prefixFiles, fmt.Sprintf("%d", chunkIndex))
|
||||
_, closer, err := f.db.Get(k)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("check file %q chunk %d: %w", id, chunkIndex, err)
|
||||
}
|
||||
closer.Close()
|
||||
if err := batch.Delete(k, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("delete chunk %d of %q: %w", chunkIndex, id, err)
|
||||
}
|
||||
foundAny = true
|
||||
chunkIndex++
|
||||
}
|
||||
|
||||
if !foundAny {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
|
||||
metaKey := key(id.String(), prefixFiles, prefixMeta)
|
||||
if err := batch.Delete(metaKey, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("delete meta for %q: %w", id, err)
|
||||
}
|
||||
|
||||
if err := batch.Commit(pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("commit delete %q: %w", id, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *files) Size(id uuid.UUID) (int64, error) {
|
||||
metaKey := key(id.String(), prefixFiles, prefixMeta)
|
||||
data, closer, err := f.db.Get(metaKey)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
return 0, database.ErrNotFound
|
||||
}
|
||||
return 0, fmt.Errorf("size file %q: %w", id, err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
return int64(binary.LittleEndian.Uint64(data)), nil
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFiles_Put(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
|
||||
t.Run("put small file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte("hello")))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, id)
|
||||
})
|
||||
|
||||
t.Run("put empty file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, id)
|
||||
})
|
||||
|
||||
t.Run("put file larger than one chunk", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), fileChunkSize+1)
|
||||
id, err := files.Put(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, id)
|
||||
})
|
||||
|
||||
t.Run("put multiple files generates different ids", func(t *testing.T) {
|
||||
id1, err := files.Put(bytes.NewReader([]byte("first")))
|
||||
require.NoError(t, err)
|
||||
id2, err := files.Put(bytes.NewReader([]byte("second")))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, id1, id2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_Get(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
|
||||
t.Run("get existing file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte("hello world")))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(id, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", buf.String())
|
||||
})
|
||||
|
||||
t.Run("get non-existing file", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := files.Get(uuid.New(), &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("get empty file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(id, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", buf.String())
|
||||
})
|
||||
|
||||
t.Run("get multi-chunk file", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("b"), fileChunkSize+100)
|
||||
id, err := files.Put(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(id, &buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(data), buf.Len())
|
||||
require.Equal(t, data, buf.Bytes())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_Delete(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
|
||||
t.Run("delete existing file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte("delete me")))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify deleted.
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(id, &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete non-existing file", func(t *testing.T) {
|
||||
err := files.Delete(uuid.New())
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("delete multi-chunk file", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("c"), fileChunkSize*2+50)
|
||||
id, err := files.Put(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = files.Delete(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = files.Get(id, &buf)
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_Size(t *testing.T) {
|
||||
db, cleanup := openDB(t)
|
||||
defer cleanup()
|
||||
|
||||
files := db.Files()
|
||||
|
||||
t.Run("size of existing file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte("hello")))
|
||||
require.NoError(t, err)
|
||||
|
||||
size, err := files.Size(id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(5), size)
|
||||
})
|
||||
|
||||
t.Run("size of non-existing file", func(t *testing.T) {
|
||||
_, err := files.Size(uuid.New())
|
||||
require.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("size of empty file", func(t *testing.T) {
|
||||
id, err := files.Put(bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
size, err := files.Size(id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), size)
|
||||
})
|
||||
|
||||
t.Run("size of multi-chunk file", func(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("d"), fileChunkSize*2+777)
|
||||
id, err := files.Put(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
size, err := files.Size(id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(len(data)), size)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFiles_AfterClosePanics(t *testing.T) {
|
||||
path := t.TempDir() + "/db"
|
||||
db, err := New(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
files := db.Files()
|
||||
id, err := files.Put(bytes.NewReader([]byte("data")))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
require.Panics(t, func() {
|
||||
_, _ = files.Put(bytes.NewReader([]byte("more")))
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
var buf bytes.Buffer
|
||||
_ = files.Get(id, &buf)
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
_ = files.Delete(id)
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
_, _ = files.Size(id)
|
||||
})
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
type messages struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
type posts struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
type reactions struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/d1nch8g/mesh/database"
|
||||
)
|
||||
|
||||
type subscriptions struct {
|
||||
db *pebble.DB
|
||||
}
|
||||
|
||||
func (s *subscriptions) Subscribe(user, targetEmail string, sig []byte) error {
|
||||
batch := s.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
if err := batch.Set(key(user, prefixSubs, prefixOut, targetEmail), sig, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("store outgoing subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
if err := batch.Set(key(targetEmail, prefixSubs, prefixIn, user), sig, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("store incoming subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
|
||||
if err := batch.Commit(pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("commit subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) Unsubscribe(user, targetEmail string) error {
|
||||
outKey := key(user, prefixSubs, prefixOut, targetEmail)
|
||||
|
||||
_, closer, err := s.db.Get(outKey)
|
||||
if err != nil {
|
||||
if err == pebble.ErrNotFound {
|
||||
return database.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("check subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
closer.Close()
|
||||
|
||||
batch := s.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
if err := batch.Delete(outKey, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("delete outgoing subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
if err := batch.Delete(key(targetEmail, prefixSubs, prefixIn, user), pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("delete incoming subscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
|
||||
if err := batch.Commit(pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("commit unsubscription %q -> %q: %w", user, targetEmail, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) OfUser(user string) ([]database.Subscription, error) {
|
||||
prefix := key(user, prefixSubs, prefixOut)
|
||||
|
||||
iter, err := s.db.NewIter(&pebble.IterOptions{
|
||||
LowerBound: prefix,
|
||||
UpperBound: append(prefix, 0xFF),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan subscriptions of %q: %w", user, err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
var subs []database.Subscription
|
||||
for iter.First(); iter.Valid(); iter.Next() {
|
||||
keyParts := bytes.Split(iter.Key(), []byte(":"))
|
||||
targetEmail := string(keyParts[len(keyParts)-1])
|
||||
|
||||
subs = append(subs, database.Subscription{
|
||||
Email: targetEmail,
|
||||
Signature: iter.Value(),
|
||||
})
|
||||
}
|
||||
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) ToUser(targetEmail string) ([]database.Subscription, error) {
|
||||
prefix := key(targetEmail, prefixSubs, prefixIn)
|
||||
|
||||
iter, err := s.db.NewIter(&pebble.IterOptions{
|
||||
LowerBound: prefix,
|
||||
UpperBound: append(prefix, 0xFF),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan subscribers to %q: %w", targetEmail, err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
var subs []database.Subscription
|
||||
for iter.First(); iter.Valid(); iter.Next() {
|
||||
keyParts := bytes.Split(iter.Key(), []byte(":"))
|
||||
subscriberEmail := string(keyParts[len(keyParts)-1])
|
||||
|
||||
subs = append(subs, database.Subscription{
|
||||
Email: subscriberEmail,
|
||||
Signature: iter.Value(),
|
||||
})
|
||||
}
|
||||
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) UpdateReference(user, domain string, delta int) error {
|
||||
domainKey := key(user, prefixSubs, prefixRef, domain)
|
||||
var domainCount int64
|
||||
data, closer, err := s.db.Get(domainKey)
|
||||
if err == nil {
|
||||
domainCount = int64(binary.LittleEndian.Uint64(data))
|
||||
closer.Close()
|
||||
} else if err != pebble.ErrNotFound {
|
||||
return fmt.Errorf("read reference %q -> %q: %w", user, domain, err)
|
||||
}
|
||||
|
||||
newDomainCount := max(domainCount+int64(delta), 0)
|
||||
|
||||
totalKey := key(user, prefixSubs, prefixTotal)
|
||||
var totalCount int64
|
||||
data, closer, err = s.db.Get(totalKey)
|
||||
if err == nil {
|
||||
totalCount = int64(binary.LittleEndian.Uint64(data))
|
||||
closer.Close()
|
||||
} else if err != pebble.ErrNotFound {
|
||||
return fmt.Errorf("read total for %q: %w", user, err)
|
||||
}
|
||||
|
||||
newTotal := max(totalCount+int64(delta), 0)
|
||||
|
||||
batch := s.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
if newDomainCount == 0 {
|
||||
if err := batch.Delete(domainKey, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("delete reference %q -> %q: %w", user, domain, err)
|
||||
}
|
||||
} else {
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], uint64(newDomainCount))
|
||||
if err := batch.Set(domainKey, buf[:], pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("store reference %q -> %q: %w", user, domain, err)
|
||||
}
|
||||
}
|
||||
|
||||
if newTotal == 0 {
|
||||
if err := batch.Delete(totalKey, pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("delete total for %q: %w", user, err)
|
||||
}
|
||||
} else {
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], uint64(newTotal))
|
||||
if err := batch.Set(totalKey, buf[:], pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("store total for %q: %w", user, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := batch.Commit(pebble.NoSync); err != nil {
|
||||
return fmt.Errorf("commit reference update for %q -> %q: %w", user, domain, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) ListReferences(user string) ([]database.SubscriptionReference, error) {
|
||||
prefix := key(user, prefixSubs, prefixRef)
|
||||
|
||||
iter, err := s.db.NewIter(&pebble.IterOptions{
|
||||
LowerBound: prefix,
|
||||
UpperBound: append(prefix, 0xFF),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan references for %q: %w", user, err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
var refs []database.SubscriptionReference
|
||||
for iter.First(); iter.Valid(); iter.Next() {
|
||||
domain := string(iter.Key()[len(prefix)+1:])
|
||||
count := binary.LittleEndian.Uint64(iter.Value())
|
||||
|
||||
refs = append(refs, database.SubscriptionReference{
|
||||
Domain: domain,
|
||||
Count: int(count),
|
||||
})
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
@@ -3,39 +3,14 @@ module github.com/d1nch8g/mesh
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/cockroachdb/pebble v1.1.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DataDog/zstd v1.4.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cockroachdb/errors v1.11.3 // indirect
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
|
||||
github.com/cockroachdb/redact v1.1.5 // indirect
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/getsentry/sentry-go v0.27.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.16.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.15.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,115 +1,38 @@
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
|
||||
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
|
||||
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
|
||||
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
Reference in New Issue
Block a user