feat(storage): rpc based implementation
All checks were successful
arcad/edge/pipeline/pr-master This commit looks good
All checks were successful
arcad/edge/pipeline/pr-master This commit looks good
This commit is contained in:
35
pkg/storage/driver/blob_store.go
Normal file
35
pkg/storage/driver/blob_store.go
Normal file
@ -0,0 +1,35 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var blobStoreFactories = make(map[string]BlobStoreFactory, 0)
|
||||
|
||||
type BlobStoreFactory func(url *url.URL) (storage.BlobStore, error)
|
||||
|
||||
func RegisterBlobStoreFactory(scheme string, factory BlobStoreFactory) {
|
||||
blobStoreFactories[scheme] = factory
|
||||
}
|
||||
|
||||
func NewBlobStore(dsn string) (storage.BlobStore, error) {
|
||||
url, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
factory, exists := blobStoreFactories[url.Scheme]
|
||||
if !exists {
|
||||
return nil, errors.WithStack(ErrSchemeNotRegistered)
|
||||
}
|
||||
|
||||
store, err := factory(url)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
35
pkg/storage/driver/document_store.go
Normal file
35
pkg/storage/driver/document_store.go
Normal file
@ -0,0 +1,35 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var documentStoreFactories = make(map[string]DocumentStoreFactory, 0)
|
||||
|
||||
type DocumentStoreFactory func(url *url.URL) (storage.DocumentStore, error)
|
||||
|
||||
func RegisterDocumentStoreFactory(scheme string, factory DocumentStoreFactory) {
|
||||
documentStoreFactories[scheme] = factory
|
||||
}
|
||||
|
||||
func NewDocumentStore(dsn string) (storage.DocumentStore, error) {
|
||||
url, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
factory, exists := documentStoreFactories[url.Scheme]
|
||||
if !exists {
|
||||
return nil, errors.WithStack(ErrSchemeNotRegistered)
|
||||
}
|
||||
|
||||
store, err := factory(url)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
5
pkg/storage/driver/error.go
Normal file
5
pkg/storage/driver/error.go
Normal file
@ -0,0 +1,5 @@
|
||||
package driver
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrSchemeNotRegistered = errors.New("scheme was not registered")
|
239
pkg/storage/driver/rpc/client/blob_bucket.go
Normal file
239
pkg/storage/driver/rpc/client/blob_bucket.go
Normal file
@ -0,0 +1,239 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BlobBucket struct {
|
||||
name string
|
||||
id blob.BucketID
|
||||
call CallFunc
|
||||
}
|
||||
|
||||
// Size implements storage.BlobBucket
|
||||
func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
|
||||
args := blob.GetBucketSizeArgs{
|
||||
BucketID: b.id,
|
||||
}
|
||||
|
||||
reply := blob.GetBucketSizeReply{}
|
||||
|
||||
if err := b.call(ctx, "Service.GetBucketSize", args, &reply); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Size, nil
|
||||
}
|
||||
|
||||
// Name implements storage.BlobBucket
|
||||
func (b *BlobBucket) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
// Close implements storage.BlobBucket
|
||||
func (b *BlobBucket) Close() error {
|
||||
args := blob.CloseBucketArgs{
|
||||
BucketID: b.id,
|
||||
}
|
||||
|
||||
reply := blob.CloseBucketReply{}
|
||||
|
||||
if err := b.call(context.Background(), "Service.CloseBucket", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete implements storage.BlobBucket
|
||||
func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
|
||||
args := blob.DeleteBucketArgs{
|
||||
BucketName: b.name,
|
||||
}
|
||||
|
||||
reply := blob.DeleteBucketReply{}
|
||||
|
||||
if err := b.call(context.Background(), "Service.DeleteBucket", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implements storage.BlobBucket
|
||||
func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) {
|
||||
args := blob.GetBlobInfoArgs{
|
||||
BucketID: b.id,
|
||||
BlobID: id,
|
||||
}
|
||||
|
||||
reply := blob.GetBlobInfoReply{}
|
||||
|
||||
if err := b.call(context.Background(), "Service.GetBlobInfo", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.BlobInfo, nil
|
||||
}
|
||||
|
||||
// List implements storage.BlobBucket
|
||||
func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
|
||||
args := blob.ListBlobInfoArgs{
|
||||
BucketID: b.id,
|
||||
}
|
||||
|
||||
reply := blob.ListBlobInfoReply{}
|
||||
|
||||
if err := b.call(context.Background(), "Service.ListBlobInfo", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.BlobInfos, nil
|
||||
}
|
||||
|
||||
// NewReader implements storage.BlobBucket
|
||||
func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) {
|
||||
args := blob.NewBlobReaderArgs{
|
||||
BucketID: b.id,
|
||||
BlobID: id,
|
||||
}
|
||||
|
||||
reply := blob.NewBlobReaderReply{}
|
||||
|
||||
if err := b.call(context.Background(), "Service.NewBlobReader", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &blobReaderCloser{
|
||||
readerID: reply.ReaderID,
|
||||
call: b.call,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewWriter implements storage.BlobBucket
|
||||
func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) {
|
||||
args := blob.NewBlobWriterArgs{
|
||||
BucketID: b.id,
|
||||
BlobID: id,
|
||||
}
|
||||
|
||||
reply := blob.NewBlobWriterReply{}
|
||||
|
||||
if err := b.call(context.Background(), "Service.NewBlobWriter", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &blobWriterCloser{
|
||||
blobID: id,
|
||||
writerID: reply.WriterID,
|
||||
call: b.call,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type blobWriterCloser struct {
|
||||
blobID storage.BlobID
|
||||
writerID blob.WriterID
|
||||
call CallFunc
|
||||
}
|
||||
|
||||
// Write implements io.WriteCloser
|
||||
func (bwc *blobWriterCloser) Write(data []byte) (int, error) {
|
||||
args := blob.WriteBlobArgs{
|
||||
WriterID: bwc.writerID,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
reply := blob.WriteBlobReply{}
|
||||
|
||||
if err := bwc.call(context.Background(), "Service.WriteBlob", args, &reply); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Written, nil
|
||||
}
|
||||
|
||||
// Close implements io.WriteCloser
|
||||
func (bwc *blobWriterCloser) Close() error {
|
||||
args := blob.CloseWriterArgs{
|
||||
WriterID: bwc.writerID,
|
||||
}
|
||||
|
||||
reply := blob.CloseBucketReply{}
|
||||
|
||||
if err := bwc.call(context.Background(), "Service.CloseWriter", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type blobReaderCloser struct {
|
||||
readerID blob.ReaderID
|
||||
call func(ctx context.Context, serviceMethod string, args any, reply any) error
|
||||
}
|
||||
|
||||
// Read implements io.ReadSeekCloser
|
||||
func (brc *blobReaderCloser) Read(p []byte) (int, error) {
|
||||
args := blob.ReadBlobArgs{
|
||||
ReaderID: brc.readerID,
|
||||
Length: len(p),
|
||||
}
|
||||
|
||||
reply := blob.ReadBlobReply{}
|
||||
|
||||
if err := brc.call(context.Background(), "Service.ReadBlob", args, &reply); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
copy(p, reply.Data)
|
||||
|
||||
if reply.EOF {
|
||||
return reply.Read, io.EOF
|
||||
}
|
||||
|
||||
return reply.Read, nil
|
||||
}
|
||||
|
||||
// Seek implements io.ReadSeekCloser
|
||||
func (brc *blobReaderCloser) Seek(offset int64, whence int) (int64, error) {
|
||||
args := blob.SeekBlobArgs{
|
||||
ReaderID: brc.readerID,
|
||||
Offset: offset,
|
||||
Whence: whence,
|
||||
}
|
||||
|
||||
reply := blob.SeekBlobReply{}
|
||||
|
||||
if err := brc.call(context.Background(), "Service.SeekBlob", args, &reply); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Read, nil
|
||||
}
|
||||
|
||||
// Close implements io.ReadSeekCloser
|
||||
func (brc *blobReaderCloser) Close() error {
|
||||
args := blob.CloseReaderArgs{
|
||||
ReaderID: brc.readerID,
|
||||
}
|
||||
|
||||
reply := blob.CloseReaderReply{}
|
||||
|
||||
if err := brc.call(context.Background(), "Service.CloseReader", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ storage.BlobBucket = &BlobBucket{}
|
||||
_ storage.BlobInfo = &BlobInfo{}
|
||||
_ io.WriteCloser = &blobWriterCloser{}
|
||||
_ io.ReadSeekCloser = &blobReaderCloser{}
|
||||
)
|
40
pkg/storage/driver/rpc/client/blob_info.go
Normal file
40
pkg/storage/driver/rpc/client/blob_info.go
Normal file
@ -0,0 +1,40 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
)
|
||||
|
||||
type BlobInfo struct {
|
||||
id storage.BlobID
|
||||
bucket string
|
||||
contentType string
|
||||
modTime time.Time
|
||||
size int64
|
||||
}
|
||||
|
||||
// Bucket implements storage.BlobInfo
|
||||
func (i *BlobInfo) Bucket() string {
|
||||
return i.bucket
|
||||
}
|
||||
|
||||
// ID implements storage.BlobInfo
|
||||
func (i *BlobInfo) ID() storage.BlobID {
|
||||
return i.id
|
||||
}
|
||||
|
||||
// ContentType implements storage.BlobInfo
|
||||
func (i *BlobInfo) ContentType() string {
|
||||
return i.contentType
|
||||
}
|
||||
|
||||
// ModTime implements storage.BlobInfo
|
||||
func (i *BlobInfo) ModTime() time.Time {
|
||||
return i.modTime
|
||||
}
|
||||
|
||||
// Size implements storage.BlobInfo
|
||||
func (i *BlobInfo) Size() int64 {
|
||||
return i.size
|
||||
}
|
101
pkg/storage/driver/rpc/client/blob_store.go
Normal file
101
pkg/storage/driver/rpc/client/blob_store.go
Normal file
@ -0,0 +1,101 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/keegancsmith/rpc"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BlobStore struct {
|
||||
serverURL *url.URL
|
||||
}
|
||||
|
||||
// DeleteBucket implements storage.BlobStore.
|
||||
func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error {
|
||||
args := &blob.DeleteBucketArgs{
|
||||
BucketName: name,
|
||||
}
|
||||
|
||||
if err := s.call(ctx, "Service.DeleteBucket", args, nil); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListBuckets implements storage.BlobStore.
|
||||
func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
|
||||
args := &blob.ListBucketsArgs{}
|
||||
|
||||
reply := blob.ListBucketsReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.ListBuckets", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Buckets, nil
|
||||
}
|
||||
|
||||
// OpenBucket implements storage.BlobStore.
|
||||
func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) {
|
||||
args := &blob.OpenBucketArgs{
|
||||
BucketName: name,
|
||||
}
|
||||
reply := &blob.OpenBucketReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.OpenBucket", args, reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &BlobBucket{
|
||||
name: name,
|
||||
id: reply.BucketID,
|
||||
call: s.call,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BlobStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
|
||||
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
|
||||
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlobStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
|
||||
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := client.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, client); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBlobStore(serverURL *url.URL) *BlobStore {
|
||||
return &BlobStore{serverURL}
|
||||
}
|
||||
|
||||
var _ storage.BlobStore = &BlobStore{}
|
87
pkg/storage/driver/rpc/client/blob_store_test.go
Normal file
87
pkg/storage/driver/rpc/client/blob_store_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestBlobStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Verbose() {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
}
|
||||
|
||||
httpServer, err := startNewBlobStoreServer()
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer httpServer.Close()
|
||||
|
||||
serverAddr := httpServer.Listener.Addr()
|
||||
serverURL := &url.URL{
|
||||
Host: serverAddr.String(),
|
||||
}
|
||||
|
||||
store := NewBlobStore(serverURL)
|
||||
|
||||
testsuite.TestBlobStore(context.Background(), t, store)
|
||||
}
|
||||
|
||||
func BenchmarkBlobStore(t *testing.B) {
|
||||
logger.SetLevel(logger.LevelError)
|
||||
|
||||
httpServer, err := startNewBlobStoreServer()
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer httpServer.Close()
|
||||
|
||||
serverAddr := httpServer.Listener.Addr()
|
||||
serverURL := &url.URL{
|
||||
Host: serverAddr.String(),
|
||||
}
|
||||
|
||||
store := NewBlobStore(serverURL)
|
||||
|
||||
testsuite.BenchmarkBlobStore(t, store)
|
||||
|
||||
}
|
||||
|
||||
func getSQLiteBlobStore() (*sqlite.BlobStore, error) {
|
||||
file := "./testdata/blobstore_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := sqlite.NewBlobStore(dsn)
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func startNewBlobStoreServer() (*httptest.Server, error) {
|
||||
store, err := getSQLiteBlobStore()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
server := server.NewBlobStoreServer(store)
|
||||
|
||||
httpServer := httptest.NewServer(server)
|
||||
|
||||
return httpServer, nil
|
||||
}
|
134
pkg/storage/driver/rpc/client/document_store.go
Normal file
134
pkg/storage/driver/rpc/client/document_store.go
Normal file
@ -0,0 +1,134 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/keegancsmith/rpc"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||
)
|
||||
|
||||
type DocumentStore struct {
|
||||
serverURL *url.URL
|
||||
}
|
||||
|
||||
// Delete implements storage.DocumentStore.
|
||||
func (s *DocumentStore) Delete(ctx context.Context, collection string, id storage.DocumentID) error {
|
||||
args := document.DeleteDocumentArgs{
|
||||
Collection: collection,
|
||||
DocumentID: id,
|
||||
}
|
||||
|
||||
reply := document.DeleteDocumentReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.DeleteDocument", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implements storage.DocumentStore.
|
||||
func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.DocumentID) (storage.Document, error) {
|
||||
args := document.GetDocumentArgs{
|
||||
Collection: collection,
|
||||
DocumentID: id,
|
||||
}
|
||||
|
||||
reply := document.GetDocumentReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.GetDocument", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Document, nil
|
||||
}
|
||||
|
||||
// Query implements storage.DocumentStore.
|
||||
func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) {
|
||||
opts := &storage.QueryOptions{}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
args := document.QueryDocumentsArgs{
|
||||
Collection: collection,
|
||||
Filter: nil,
|
||||
Options: opts,
|
||||
}
|
||||
|
||||
if filter != nil {
|
||||
args.Filter = filter.AsMap()
|
||||
}
|
||||
|
||||
reply := document.QueryDocumentsReply{
|
||||
Documents: []storage.Document{},
|
||||
}
|
||||
|
||||
if err := s.call(ctx, "Service.QueryDocuments", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Documents, nil
|
||||
}
|
||||
|
||||
// Upsert implements storage.DocumentStore.
|
||||
func (s *DocumentStore) Upsert(ctx context.Context, collection string, doc storage.Document) (storage.Document, error) {
|
||||
args := document.UpsertDocumentArgs{
|
||||
Collection: collection,
|
||||
Document: doc,
|
||||
}
|
||||
|
||||
reply := document.UpsertDocumentReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.UpsertDocument", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Document, nil
|
||||
}
|
||||
|
||||
func (s *DocumentStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
|
||||
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
|
||||
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DocumentStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
|
||||
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := client.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, client); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDocumentStore(url *url.URL) *DocumentStore {
|
||||
return &DocumentStore{url}
|
||||
}
|
||||
|
||||
var _ storage.DocumentStore = &DocumentStore{}
|
67
pkg/storage/driver/rpc/client/document_store_test.go
Normal file
67
pkg/storage/driver/rpc/client/document_store_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestDocumentStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Verbose() {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
}
|
||||
|
||||
httpServer, err := startNewDocumentStoreServer()
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer httpServer.Close()
|
||||
|
||||
serverAddr := httpServer.Listener.Addr()
|
||||
|
||||
serverURL := &url.URL{
|
||||
Host: serverAddr.String(),
|
||||
}
|
||||
|
||||
store := NewDocumentStore(serverURL)
|
||||
|
||||
testsuite.TestDocumentStore(context.Background(), t, store)
|
||||
}
|
||||
|
||||
func getSQLiteDocumentStore() (*sqlite.DocumentStore, error) {
|
||||
file := "./testdata/documentstore_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := sqlite.NewDocumentStore(dsn)
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func startNewDocumentStoreServer() (*httptest.Server, error) {
|
||||
store, err := getSQLiteDocumentStore()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
server := server.NewDocumentStoreServer(store)
|
||||
|
||||
httpServer := httptest.NewServer(server)
|
||||
|
||||
return httpServer, nil
|
||||
}
|
17
pkg/storage/driver/rpc/client/error.go
Normal file
17
pkg/storage/driver/rpc/client/error.go
Normal file
@ -0,0 +1,17 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func remapShareError(err error) error {
|
||||
switch errors.Cause(err).Error() {
|
||||
case share.ErrAttributeRequired.Error():
|
||||
return share.ErrAttributeRequired
|
||||
case share.ErrNotFound.Error():
|
||||
return share.ErrNotFound
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
9
pkg/storage/driver/rpc/client/init.go
Normal file
9
pkg/storage/driver/rpc/client/init.go
Normal file
@ -0,0 +1,9 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
|
||||
)
|
||||
|
||||
type CallFunc func(ctx context.Context, serviceMethod string, args any, reply any) error
|
150
pkg/storage/driver/rpc/client/share_store.go
Normal file
150
pkg/storage/driver/rpc/client/share_store.go
Normal file
@ -0,0 +1,150 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
server "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/keegancsmith/rpc"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type ShareStore struct {
|
||||
serverURL *url.URL
|
||||
}
|
||||
|
||||
// DeleteAttributes implements share.Store.
|
||||
func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
|
||||
args := server.DeleteAttributesArgs{
|
||||
Origin: origin,
|
||||
ResourceID: resourceID,
|
||||
Names: names,
|
||||
}
|
||||
|
||||
reply := server.DeleteAttributesArgs{}
|
||||
|
||||
if err := s.call(ctx, "Service.DeleteAttributes", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteResource implements share.Store.
|
||||
func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
|
||||
args := server.DeleteResourceArgs{
|
||||
Origin: origin,
|
||||
ResourceID: resourceID,
|
||||
}
|
||||
|
||||
reply := server.DeleteResourceReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.DeleteResource", args, &reply); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindResources implements share.Store.
|
||||
func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
|
||||
options := share.NewFindResourcesOptions(funcs...)
|
||||
|
||||
args := server.FindResourcesArgs{
|
||||
Options: options,
|
||||
}
|
||||
|
||||
reply := server.FindResourcesReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.FindResources", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources := make([]share.Resource, len(reply.Resources))
|
||||
for idx, res := range reply.Resources {
|
||||
resources[idx] = res
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// GetResource implements share.Store.
|
||||
func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
|
||||
args := server.GetResourceArgs{
|
||||
Origin: origin,
|
||||
ResourceID: resourceID,
|
||||
}
|
||||
|
||||
reply := server.GetResourceReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.GetResource", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Resource, nil
|
||||
}
|
||||
|
||||
// UpdateAttributes implements share.Store.
|
||||
func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
|
||||
serializableAttributes := make([]*server.SerializableAttribute, len(attributes))
|
||||
for attrIdx, attr := range attributes {
|
||||
serializableAttributes[attrIdx] = server.FromAttribute(attr)
|
||||
}
|
||||
|
||||
args := server.UpdateAttributesArgs{
|
||||
Origin: origin,
|
||||
ResourceID: resourceID,
|
||||
Attributes: serializableAttributes,
|
||||
}
|
||||
|
||||
reply := server.UpdateAttributesReply{}
|
||||
|
||||
if err := s.call(ctx, "Service.UpdateAttributes", args, &reply); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply.Resource, nil
|
||||
}
|
||||
|
||||
func (s *ShareStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
|
||||
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
|
||||
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
|
||||
return errors.WithStack(remapShareError(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShareStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
|
||||
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := client.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, client); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewShareStore(url *url.URL) *ShareStore {
|
||||
return &ShareStore{url}
|
||||
}
|
||||
|
||||
var _ share.Store = &ShareStore{}
|
67
pkg/storage/driver/rpc/client/share_store_test.go
Normal file
67
pkg/storage/driver/rpc/client/share_store_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestShareStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Verbose() {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
}
|
||||
|
||||
testsuite.TestStore(t, func(testName string) (share.Store, error) {
|
||||
httpServer, err := startNewShareStoreServer(testName)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
serverAddr := httpServer.Listener.Addr()
|
||||
serverURL := &url.URL{
|
||||
Host: serverAddr.String(),
|
||||
}
|
||||
|
||||
return NewShareStore(serverURL), nil
|
||||
})
|
||||
}
|
||||
|
||||
func getSQLiteShareStore(testName string) (*sqlite.ShareStore, error) {
|
||||
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
|
||||
|
||||
file := fmt.Sprintf("./testdata/sharestore_test_%s.sqlite", filename)
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := sqlite.NewShareStore(dsn)
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func startNewShareStoreServer(testName string) (*httptest.Server, error) {
|
||||
store, err := getSQLiteShareStore(testName)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
server := server.NewShareStoreServer(store)
|
||||
|
||||
httpServer := httptest.NewServer(server)
|
||||
|
||||
return httpServer, nil
|
||||
}
|
22
pkg/storage/driver/rpc/driver.go
Normal file
22
pkg/storage/driver/rpc/driver.go
Normal file
@ -0,0 +1,22 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/client"
|
||||
)
|
||||
|
||||
func init() {
|
||||
driver.RegisterDocumentStoreFactory("rpc", documentStoreFactory)
|
||||
driver.RegisterBlobStoreFactory("rpc", blobStoreFactory)
|
||||
}
|
||||
|
||||
func documentStoreFactory(url *url.URL) (storage.DocumentStore, error) {
|
||||
return client.NewDocumentStore(url), nil
|
||||
}
|
||||
|
||||
func blobStoreFactory(url *url.URL) (storage.BlobStore, error) {
|
||||
return client.NewBlobStore(url), nil
|
||||
}
|
42
pkg/storage/driver/rpc/gob/blob_info.go
Normal file
42
pkg/storage/driver/rpc/gob/blob_info.go
Normal file
@ -0,0 +1,42 @@
|
||||
package gob
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
)
|
||||
|
||||
type BlobInfo struct {
|
||||
Bucket_ string
|
||||
ContentType_ string
|
||||
BlobID_ storage.BlobID
|
||||
ModTime_ time.Time
|
||||
Size_ int64
|
||||
}
|
||||
|
||||
// Bucket implements storage.BlobInfo.
|
||||
func (bi *BlobInfo) Bucket() string {
|
||||
return bi.Bucket_
|
||||
}
|
||||
|
||||
// ContentType implements storage.BlobInfo.
|
||||
func (bi *BlobInfo) ContentType() string {
|
||||
return bi.ContentType_
|
||||
}
|
||||
|
||||
// ID implements storage.BlobInfo.
|
||||
func (bi *BlobInfo) ID() storage.BlobID {
|
||||
return bi.BlobID_
|
||||
}
|
||||
|
||||
// ModTime implements storage.BlobInfo.
|
||||
func (bi *BlobInfo) ModTime() time.Time {
|
||||
return bi.ModTime_
|
||||
}
|
||||
|
||||
// Size implements storage.BlobInfo.
|
||||
func (bi *BlobInfo) Size() int64 {
|
||||
return bi.Size_
|
||||
}
|
||||
|
||||
var _ storage.BlobInfo = &BlobInfo{}
|
18
pkg/storage/driver/rpc/gob/init.go
Normal file
18
pkg/storage/driver/rpc/gob/init.go
Normal file
@ -0,0 +1,18 @@
|
||||
package gob
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(storage.Document{})
|
||||
gob.Register(storage.DocumentID(""))
|
||||
gob.Register(time.Time{})
|
||||
gob.Register(map[string]interface{}{})
|
||||
gob.Register([]interface{}{})
|
||||
gob.Register([]map[string]interface{}{})
|
||||
gob.Register(&BlobInfo{})
|
||||
}
|
31
pkg/storage/driver/rpc/server/blob/close_bucket.go
Normal file
31
pkg/storage/driver/rpc/server/blob/close_bucket.go
Normal file
@ -0,0 +1,31 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CloseBucketArgs struct {
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
type CloseBucketReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) CloseBucket(ctx context.Context, args *CloseBucketArgs, reply *CloseBucketReply) error {
|
||||
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := bucket.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.buckets.Delete(args.BucketID)
|
||||
|
||||
*reply = CloseBucketReply{}
|
||||
|
||||
return nil
|
||||
}
|
31
pkg/storage/driver/rpc/server/blob/close_reader.go
Normal file
31
pkg/storage/driver/rpc/server/blob/close_reader.go
Normal file
@ -0,0 +1,31 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CloseReaderArgs struct {
|
||||
ReaderID ReaderID
|
||||
}
|
||||
|
||||
type CloseReaderReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) CloseReader(ctx context.Context, args *CloseReaderArgs, reply *CloseReaderReply) error {
|
||||
reader, err := s.getOpenedReader(args.ReaderID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := reader.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.readers.Delete(args.ReaderID)
|
||||
|
||||
*reply = CloseReaderReply{}
|
||||
|
||||
return nil
|
||||
}
|
31
pkg/storage/driver/rpc/server/blob/close_writer.go
Normal file
31
pkg/storage/driver/rpc/server/blob/close_writer.go
Normal file
@ -0,0 +1,31 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CloseWriterArgs struct {
|
||||
WriterID WriterID
|
||||
}
|
||||
|
||||
type CloseWriterReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) CloseWriter(ctx context.Context, args *CloseWriterArgs, reply *CloseWriterReply) error {
|
||||
writer, err := s.getOpenedWriter(args.WriterID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.writers.Delete(args.WriterID)
|
||||
|
||||
*reply = CloseWriterReply{}
|
||||
|
||||
return nil
|
||||
}
|
22
pkg/storage/driver/rpc/server/blob/delete_bucket.go
Normal file
22
pkg/storage/driver/rpc/server/blob/delete_bucket.go
Normal file
@ -0,0 +1,22 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DeleteBucketArgs struct {
|
||||
BucketName string
|
||||
}
|
||||
|
||||
type DeleteBucketReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) DeleteBucket(ctx context.Context, args *DeleteBucketArgs, reply *DeleteBucketReply) error {
|
||||
if err := s.store.DeleteBucket(ctx, args.BucketName); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
42
pkg/storage/driver/rpc/server/blob/get_blob_info.go
Normal file
42
pkg/storage/driver/rpc/server/blob/get_blob_info.go
Normal file
@ -0,0 +1,42 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetBlobInfoArgs struct {
|
||||
BlobID storage.BlobID
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
type GetBlobInfoReply struct {
|
||||
BlobInfo storage.BlobInfo
|
||||
}
|
||||
|
||||
func (s *Service) GetBlobInfo(ctx context.Context, args *GetBlobInfoArgs, reply *GetBlobInfoReply) error {
|
||||
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blobInfo, err := bucket.Get(ctx, args.BlobID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = GetBlobInfoReply{
|
||||
BlobInfo: &gob.BlobInfo{
|
||||
Bucket_: blobInfo.Bucket(),
|
||||
ContentType_: blobInfo.ContentType(),
|
||||
BlobID_: blobInfo.ID(),
|
||||
ModTime_: blobInfo.ModTime(),
|
||||
Size_: blobInfo.Size(),
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
33
pkg/storage/driver/rpc/server/blob/get_bucket_size.go
Normal file
33
pkg/storage/driver/rpc/server/blob/get_bucket_size.go
Normal file
@ -0,0 +1,33 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetBucketSizeArgs struct {
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
type GetBucketSizeReply struct {
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (s *Service) GetBucketSize(ctx context.Context, args *GetBucketSizeArgs, reply *GetBucketSizeReply) error {
|
||||
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
size, err := bucket.Size(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = GetBucketSizeReply{
|
||||
Size: size,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
34
pkg/storage/driver/rpc/server/blob/list_blob_info.go
Normal file
34
pkg/storage/driver/rpc/server/blob/list_blob_info.go
Normal file
@ -0,0 +1,34 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ListBlobInfoArgs struct {
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
type ListBlobInfoReply struct {
|
||||
BlobInfos []storage.BlobInfo
|
||||
}
|
||||
|
||||
func (s *Service) ListBlobInfo(ctx context.Context, args *ListBlobInfoArgs, reply *ListBlobInfoReply) error {
|
||||
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blobInfos, err := bucket.List(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = ListBlobInfoReply{
|
||||
BlobInfos: blobInfos,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
27
pkg/storage/driver/rpc/server/blob/list_buckets.go
Normal file
27
pkg/storage/driver/rpc/server/blob/list_buckets.go
Normal file
@ -0,0 +1,27 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ListBucketsArgs struct {
|
||||
}
|
||||
|
||||
type ListBucketsReply struct {
|
||||
Buckets []string
|
||||
}
|
||||
|
||||
func (s *Service) ListBuckets(ctx context.Context, args *ListBucketsArgs, reply *ListBucketsReply) error {
|
||||
buckets, err := s.store.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = ListBucketsReply{
|
||||
Buckets: buckets,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
57
pkg/storage/driver/rpc/server/blob/new_blob_reader.go
Normal file
57
pkg/storage/driver/rpc/server/blob/new_blob_reader.go
Normal file
@ -0,0 +1,57 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type NewBlobReaderArgs struct {
|
||||
BlobID storage.BlobID
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
type NewBlobReaderReply struct {
|
||||
ReaderID ReaderID
|
||||
}
|
||||
|
||||
func (s *Service) NewBlobReader(ctx context.Context, args *NewBlobReaderArgs, reply *NewBlobReaderReply) error {
|
||||
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
readerID, err := NewReaderID()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
reader, err := bucket.NewReader(ctx, args.BlobID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.readers.Store(readerID, reader)
|
||||
|
||||
*reply = NewBlobReaderReply{
|
||||
ReaderID: readerID,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) getOpenedReader(id ReaderID) (io.ReadSeekCloser, error) {
|
||||
raw, exists := s.readers.Load(id)
|
||||
if !exists {
|
||||
return nil, errors.Errorf("could not find writer '%s'", id)
|
||||
}
|
||||
|
||||
reader, ok := raw.(io.ReadSeekCloser)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type '%T' for writer", raw)
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
57
pkg/storage/driver/rpc/server/blob/new_blob_writer.go
Normal file
57
pkg/storage/driver/rpc/server/blob/new_blob_writer.go
Normal file
@ -0,0 +1,57 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type NewBlobWriterArgs struct {
|
||||
BlobID storage.BlobID
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
type NewBlobWriterReply struct {
|
||||
WriterID WriterID
|
||||
}
|
||||
|
||||
func (s *Service) NewBlobWriter(ctx context.Context, args *NewBlobWriterArgs, reply *NewBlobWriterReply) error {
|
||||
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
writerID, err := NewWriterID()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
writer, err := bucket.NewWriter(ctx, args.BlobID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.writers.Store(writerID, writer)
|
||||
|
||||
*reply = NewBlobWriterReply{
|
||||
WriterID: writerID,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) getOpenedWriter(id WriterID) (io.WriteCloser, error) {
|
||||
raw, exists := s.writers.Load(id)
|
||||
if !exists {
|
||||
return nil, errors.Errorf("could not find writer '%s'", id)
|
||||
}
|
||||
|
||||
writer, ok := raw.(io.WriteCloser)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type '%T' for writer", raw)
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
50
pkg/storage/driver/rpc/server/blob/open_bucket.go
Normal file
50
pkg/storage/driver/rpc/server/blob/open_bucket.go
Normal file
@ -0,0 +1,50 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type OpenBucketArgs struct {
|
||||
BucketName string
|
||||
}
|
||||
|
||||
type OpenBucketReply struct {
|
||||
BucketID BucketID
|
||||
}
|
||||
|
||||
func (s *Service) OpenBucket(ctx context.Context, args *OpenBucketArgs, reply *OpenBucketReply) error {
|
||||
bucket, err := s.store.OpenBucket(ctx, args.BucketName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bucketID, err := NewBucketID()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.buckets.Store(bucketID, bucket)
|
||||
|
||||
*reply = OpenBucketReply{
|
||||
BucketID: bucketID,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) getOpenedBucket(id BucketID) (storage.BlobBucket, error) {
|
||||
raw, exists := s.buckets.Load(id)
|
||||
if !exists {
|
||||
return nil, errors.WithStack(storage.ErrBucketClosed)
|
||||
}
|
||||
|
||||
bucket, ok := raw.(storage.BlobBucket)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type '%T' for blob bucket", raw)
|
||||
}
|
||||
|
||||
return bucket, nil
|
||||
}
|
41
pkg/storage/driver/rpc/server/blob/read_blob.go
Normal file
41
pkg/storage/driver/rpc/server/blob/read_blob.go
Normal file
@ -0,0 +1,41 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ReadBlobArgs struct {
|
||||
ReaderID ReaderID
|
||||
Length int
|
||||
}
|
||||
|
||||
type ReadBlobReply struct {
|
||||
Data []byte
|
||||
Read int
|
||||
EOF bool
|
||||
}
|
||||
|
||||
func (s *Service) ReadBlob(ctx context.Context, args *ReadBlobArgs, reply *ReadBlobReply) error {
|
||||
reader, err := s.getOpenedReader(args.ReaderID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
buff := make([]byte, args.Length)
|
||||
|
||||
read, err := reader.Read(buff)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = ReadBlobReply{
|
||||
Read: read,
|
||||
Data: buff,
|
||||
EOF: errors.Is(err, io.EOF),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
38
pkg/storage/driver/rpc/server/blob/seek_blob.go
Normal file
38
pkg/storage/driver/rpc/server/blob/seek_blob.go
Normal file
@ -0,0 +1,38 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SeekBlobArgs struct {
|
||||
ReaderID ReaderID
|
||||
Offset int64
|
||||
Whence int
|
||||
}
|
||||
|
||||
type SeekBlobReply struct {
|
||||
Read int64
|
||||
EOF bool
|
||||
}
|
||||
|
||||
func (s *Service) SeekBlob(ctx context.Context, args *SeekBlobArgs, reply *SeekBlobReply) error {
|
||||
reader, err := s.getOpenedReader(args.ReaderID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
read, err := reader.Seek(args.Offset, args.Whence)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = SeekBlobReply{
|
||||
Read: read,
|
||||
EOF: errors.Is(err, io.EOF),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
60
pkg/storage/driver/rpc/server/blob/service.go
Normal file
60
pkg/storage/driver/rpc/server/blob/service.go
Normal file
@ -0,0 +1,60 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BucketID string
|
||||
type WriterID string
|
||||
type ReaderID string
|
||||
|
||||
type Service struct {
|
||||
store storage.BlobStore
|
||||
buckets sync.Map
|
||||
writers sync.Map
|
||||
readers sync.Map
|
||||
}
|
||||
|
||||
func NewService(store storage.BlobStore) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBucketID() (BucketID, error) {
|
||||
uuid, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
id := BucketID(fmt.Sprintf("bucket-%s", uuid.String()))
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func NewWriterID() (WriterID, error) {
|
||||
uuid, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
id := WriterID(fmt.Sprintf("writer-%s", uuid.String()))
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func NewReaderID() (ReaderID, error) {
|
||||
uuid, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
id := ReaderID(fmt.Sprintf("reader-%s", uuid.String()))
|
||||
|
||||
return id, nil
|
||||
}
|
34
pkg/storage/driver/rpc/server/blob/write_blob.go
Normal file
34
pkg/storage/driver/rpc/server/blob/write_blob.go
Normal file
@ -0,0 +1,34 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type WriteBlobArgs struct {
|
||||
WriterID WriterID
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type WriteBlobReply struct {
|
||||
Written int
|
||||
}
|
||||
|
||||
func (s *Service) WriteBlob(ctx context.Context, args *WriteBlobArgs, reply *WriteBlobReply) error {
|
||||
writer, err := s.getOpenedWriter(args.WriterID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
written, err := writer.Write(args.Data)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = WriteBlobReply{
|
||||
Written: written,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
26
pkg/storage/driver/rpc/server/document/delete_document.go
Normal file
26
pkg/storage/driver/rpc/server/document/delete_document.go
Normal file
@ -0,0 +1,26 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DeleteDocumentArgs struct {
|
||||
Collection string
|
||||
DocumentID storage.DocumentID
|
||||
}
|
||||
|
||||
type DeleteDocumentReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) DeleteDocument(ctx context.Context, args DeleteDocumentArgs, reply *DeleteDocumentReply) error {
|
||||
if err := s.store.Delete(ctx, args.Collection, args.DocumentID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = DeleteDocumentReply{}
|
||||
|
||||
return nil
|
||||
}
|
30
pkg/storage/driver/rpc/server/document/get_document.go
Normal file
30
pkg/storage/driver/rpc/server/document/get_document.go
Normal file
@ -0,0 +1,30 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetDocumentArgs struct {
|
||||
Collection string
|
||||
DocumentID storage.DocumentID
|
||||
}
|
||||
|
||||
type GetDocumentReply struct {
|
||||
Document storage.Document
|
||||
}
|
||||
|
||||
func (s *Service) GetDocument(ctx context.Context, args GetDocumentArgs, reply *GetDocumentReply) error {
|
||||
document, err := s.store.Get(ctx, args.Collection, args.DocumentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = GetDocumentReply{
|
||||
Document: document,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
53
pkg/storage/driver/rpc/server/document/query_documents.go
Normal file
53
pkg/storage/driver/rpc/server/document/query_documents.go
Normal file
@ -0,0 +1,53 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type QueryDocumentsArgs struct {
|
||||
Collection string
|
||||
Filter map[string]any
|
||||
Options *storage.QueryOptions
|
||||
}
|
||||
|
||||
type QueryDocumentsReply struct {
|
||||
Documents []storage.Document
|
||||
}
|
||||
|
||||
func (s *Service) QueryDocuments(ctx context.Context, args QueryDocumentsArgs, reply *QueryDocumentsReply) error {
|
||||
var (
|
||||
argsFilter *filter.Filter
|
||||
err error
|
||||
)
|
||||
|
||||
if args.Filter != nil {
|
||||
argsFilter, err = filter.NewFrom(args.Filter)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
documents, err := s.store.Query(ctx, args.Collection, argsFilter, withQueryOptions(args.Options))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = QueryDocumentsReply{
|
||||
Documents: documents,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func withQueryOptions(opts *storage.QueryOptions) storage.QueryOptionFunc {
|
||||
return func(o *storage.QueryOptions) {
|
||||
o.Limit = opts.Limit
|
||||
o.Offset = opts.Offset
|
||||
o.OrderBy = opts.OrderBy
|
||||
o.OrderDirection = opts.OrderDirection
|
||||
}
|
||||
}
|
11
pkg/storage/driver/rpc/server/document/service.go
Normal file
11
pkg/storage/driver/rpc/server/document/service.go
Normal file
@ -0,0 +1,11 @@
|
||||
package document
|
||||
|
||||
import "forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
|
||||
type Service struct {
|
||||
store storage.DocumentStore
|
||||
}
|
||||
|
||||
func NewService(store storage.DocumentStore) *Service {
|
||||
return &Service{store}
|
||||
}
|
30
pkg/storage/driver/rpc/server/document/upsert_document.go
Normal file
30
pkg/storage/driver/rpc/server/document/upsert_document.go
Normal file
@ -0,0 +1,30 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UpsertDocumentArgs struct {
|
||||
Collection string
|
||||
Document storage.Document
|
||||
}
|
||||
|
||||
type UpsertDocumentReply struct {
|
||||
Document storage.Document
|
||||
}
|
||||
|
||||
func (s *Service) UpsertDocument(ctx context.Context, args UpsertDocumentArgs, reply *UpsertDocumentReply) error {
|
||||
document, err := s.store.Upsert(ctx, args.Collection, args.Document)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = UpsertDocumentReply{
|
||||
Document: document,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
5
pkg/storage/driver/rpc/server/init.go
Normal file
5
pkg/storage/driver/rpc/server/init.go
Normal file
@ -0,0 +1,5 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
|
||||
)
|
29
pkg/storage/driver/rpc/server/server.go
Normal file
29
pkg/storage/driver/rpc/server/server.go
Normal file
@ -0,0 +1,29 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/keegancsmith/rpc"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
|
||||
shareService "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
)
|
||||
|
||||
func NewBlobStoreServer(store storage.BlobStore) *rpc.Server {
|
||||
server := rpc.NewServer()
|
||||
server.Register(blob.NewService(store))
|
||||
return server
|
||||
}
|
||||
|
||||
func NewDocumentStoreServer(store storage.DocumentStore) *rpc.Server {
|
||||
server := rpc.NewServer()
|
||||
server.Register(document.NewService(store))
|
||||
return server
|
||||
}
|
||||
|
||||
func NewShareStoreServer(store share.Store) *rpc.Server {
|
||||
server := rpc.NewServer()
|
||||
server.Register(shareService.NewService(store))
|
||||
return server
|
||||
}
|
28
pkg/storage/driver/rpc/server/share/delete_attributes.go
Normal file
28
pkg/storage/driver/rpc/server/share/delete_attributes.go
Normal file
@ -0,0 +1,28 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DeleteAttributesArgs struct {
|
||||
Origin app.ID
|
||||
ResourceID share.ResourceID
|
||||
Names []string
|
||||
}
|
||||
|
||||
type DeleteAttributesReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAttributes(ctx context.Context, args DeleteAttributesArgs, reply *DeleteAttributesReply) error {
|
||||
if err := s.store.DeleteAttributes(ctx, args.Origin, args.ResourceID, args.Names...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = DeleteAttributesReply{}
|
||||
|
||||
return nil
|
||||
}
|
27
pkg/storage/driver/rpc/server/share/delete_resource.go
Normal file
27
pkg/storage/driver/rpc/server/share/delete_resource.go
Normal file
@ -0,0 +1,27 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DeleteResourceArgs struct {
|
||||
Origin app.ID
|
||||
ResourceID share.ResourceID
|
||||
}
|
||||
|
||||
type DeleteResourceReply struct {
|
||||
}
|
||||
|
||||
func (s *Service) DeleteResource(ctx context.Context, args DeleteResourceArgs, reply *DeleteResourceReply) error {
|
||||
if err := s.store.DeleteResource(ctx, args.Origin, args.ResourceID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = DeleteResourceReply{}
|
||||
|
||||
return nil
|
||||
}
|
41
pkg/storage/driver/rpc/server/share/find_resources.go
Normal file
41
pkg/storage/driver/rpc/server/share/find_resources.go
Normal file
@ -0,0 +1,41 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type FindResourcesArgs struct {
|
||||
Options *share.FindResourcesOptions
|
||||
}
|
||||
|
||||
type FindResourcesReply struct {
|
||||
Resources []*SerializableResource
|
||||
}
|
||||
|
||||
func (s *Service) FindResources(ctx context.Context, args FindResourcesArgs, reply *FindResourcesReply) error {
|
||||
resources, err := s.store.FindResources(ctx, withFindResourcesOptions(args.Options))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
serializableResources := make([]*SerializableResource, len(resources))
|
||||
for resIdx, r := range resources {
|
||||
serializableResources[resIdx] = FromResource(r)
|
||||
}
|
||||
|
||||
*reply = FindResourcesReply{
|
||||
Resources: serializableResources,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func withFindResourcesOptions(opts *share.FindResourcesOptions) share.FindResourcesOptionFunc {
|
||||
return func(o *share.FindResourcesOptions) {
|
||||
o.Name = opts.Name
|
||||
o.ValueType = opts.ValueType
|
||||
}
|
||||
}
|
31
pkg/storage/driver/rpc/server/share/get_resource.go
Normal file
31
pkg/storage/driver/rpc/server/share/get_resource.go
Normal file
@ -0,0 +1,31 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetResourceArgs struct {
|
||||
Origin app.ID
|
||||
ResourceID share.ResourceID
|
||||
}
|
||||
|
||||
type GetResourceReply struct {
|
||||
Resource *SerializableResource
|
||||
}
|
||||
|
||||
func (s *Service) GetResource(ctx context.Context, args GetResourceArgs, reply *GetResourceReply) error {
|
||||
resource, err := s.store.GetResource(ctx, args.Origin, args.ResourceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = GetResourceReply{
|
||||
Resource: FromResource(resource),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
94
pkg/storage/driver/rpc/server/share/serializable.go
Normal file
94
pkg/storage/driver/rpc/server/share/serializable.go
Normal file
@ -0,0 +1,94 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
)
|
||||
|
||||
func FromResource(res share.Resource) *SerializableResource {
|
||||
serializableAttributes := make([]*SerializableAttribute, len(res.Attributes()))
|
||||
for attrIdx, attr := range res.Attributes() {
|
||||
serializableAttributes[attrIdx] = FromAttribute(attr)
|
||||
}
|
||||
|
||||
return &SerializableResource{
|
||||
ID_: res.ID(),
|
||||
Origin_: res.Origin(),
|
||||
Attributes_: serializableAttributes,
|
||||
}
|
||||
}
|
||||
|
||||
func FromAttribute(attr share.Attribute) *SerializableAttribute {
|
||||
return &SerializableAttribute{
|
||||
Name_: attr.Name(),
|
||||
Value_: attr.Value(),
|
||||
Type_: attr.Type(),
|
||||
UpdatedAt_: attr.UpdatedAt(),
|
||||
CreatedAt_: attr.CreatedAt(),
|
||||
}
|
||||
}
|
||||
|
||||
type SerializableResource struct {
|
||||
ID_ share.ResourceID
|
||||
Origin_ app.ID
|
||||
Attributes_ []*SerializableAttribute
|
||||
}
|
||||
|
||||
// Attributes implements share.Resource.
|
||||
func (r *SerializableResource) Attributes() []share.Attribute {
|
||||
attributes := make([]share.Attribute, len(r.Attributes_))
|
||||
for idx, attr := range r.Attributes_ {
|
||||
attributes[idx] = attr
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
// ID implements share.Resource.
|
||||
func (r *SerializableResource) ID() share.ResourceID {
|
||||
return r.ID_
|
||||
}
|
||||
|
||||
// Origin implements share.Resource.
|
||||
func (r *SerializableResource) Origin() app.ID {
|
||||
return r.Origin_
|
||||
}
|
||||
|
||||
var _ share.Resource = &SerializableResource{}
|
||||
|
||||
type SerializableAttribute struct {
|
||||
Name_ string
|
||||
Value_ any
|
||||
Type_ share.ValueType
|
||||
UpdatedAt_ time.Time
|
||||
CreatedAt_ time.Time
|
||||
}
|
||||
|
||||
// CreatedAt implements share.Attribute.
|
||||
func (a *SerializableAttribute) CreatedAt() time.Time {
|
||||
return a.CreatedAt_
|
||||
}
|
||||
|
||||
// Name implements share.Attribute.
|
||||
func (a *SerializableAttribute) Name() string {
|
||||
return a.Name_
|
||||
}
|
||||
|
||||
// Type implements share.Attribute.
|
||||
func (a *SerializableAttribute) Type() share.ValueType {
|
||||
return a.Type_
|
||||
}
|
||||
|
||||
// UpdatedAt implements share.Attribute.
|
||||
func (a *SerializableAttribute) UpdatedAt() time.Time {
|
||||
return a.UpdatedAt_
|
||||
}
|
||||
|
||||
// Value implements share.Attribute.
|
||||
func (a *SerializableAttribute) Value() any {
|
||||
return a.Value_
|
||||
}
|
||||
|
||||
var _ share.Attribute = &SerializableAttribute{}
|
13
pkg/storage/driver/rpc/server/share/service.go
Normal file
13
pkg/storage/driver/rpc/server/share/service.go
Normal file
@ -0,0 +1,13 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store share.Store
|
||||
}
|
||||
|
||||
func NewService(store share.Store) *Service {
|
||||
return &Service{store}
|
||||
}
|
37
pkg/storage/driver/rpc/server/share/update_attributes.go
Normal file
37
pkg/storage/driver/rpc/server/share/update_attributes.go
Normal file
@ -0,0 +1,37 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UpdateAttributesArgs struct {
|
||||
Origin app.ID
|
||||
ResourceID share.ResourceID
|
||||
Attributes []*SerializableAttribute
|
||||
}
|
||||
|
||||
type UpdateAttributesReply struct {
|
||||
Resource *SerializableResource
|
||||
}
|
||||
|
||||
func (s *Service) UpdateAttributes(ctx context.Context, args UpdateAttributesArgs, reply *UpdateAttributesReply) error {
|
||||
attributes := make([]share.Attribute, len(args.Attributes))
|
||||
for idx, attr := range args.Attributes {
|
||||
attributes[idx] = attr
|
||||
}
|
||||
|
||||
resource, err := s.store.UpdateAttributes(ctx, args.Origin, args.ResourceID, attributes...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
*reply = UpdateAttributesReply{
|
||||
Resource: FromResource(resource),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
35
pkg/storage/driver/share_store.go
Normal file
35
pkg/storage/driver/share_store.go
Normal file
@ -0,0 +1,35 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var shareStoreFactories = make(map[string]ShareStoreFactory, 0)
|
||||
|
||||
type ShareStoreFactory func(url *url.URL) (share.Store, error)
|
||||
|
||||
func RegisterShareStoreFactory(scheme string, factory ShareStoreFactory) {
|
||||
shareStoreFactories[scheme] = factory
|
||||
}
|
||||
|
||||
func NewShareStore(dsn string) (share.Store, error) {
|
||||
url, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
factory, exists := shareStoreFactories[url.Scheme]
|
||||
if !exists {
|
||||
return nil, errors.WithStack(ErrSchemeNotRegistered)
|
||||
}
|
||||
|
||||
store, err := factory(url)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
46
pkg/storage/driver/sqlite/blob_store_test.go
Normal file
46
pkg/storage/driver/sqlite/blob_store_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestBlobStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Verbose() {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
}
|
||||
|
||||
file := "./testdata/blobstore_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewBlobStore(dsn)
|
||||
|
||||
testsuite.TestBlobStore(context.Background(), t, store)
|
||||
}
|
||||
|
||||
func BenchmarkBlobStore(t *testing.B) {
|
||||
logger.SetLevel(logger.LevelError)
|
||||
|
||||
file := "./testdata/blobstore_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewBlobStore(dsn)
|
||||
|
||||
testsuite.BenchmarkBlobStore(t, store)
|
||||
}
|
@ -276,7 +276,7 @@ func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTables(ctx context.Context, db *sql.DB) error {
|
||||
func ensureDocumentTables(ctx context.Context, db *sql.DB) error {
|
||||
err := WithTx(ctx, db, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
@ -344,7 +344,7 @@ func withLimitOffsetClause(query string, args []any, limit int, offset int) (str
|
||||
}
|
||||
|
||||
func NewDocumentStore(path string) *DocumentStore {
|
||||
getDB := NewGetDBFunc(path, ensureTables)
|
||||
getDB := NewGetDBFunc(path, ensureDocumentTables)
|
||||
|
||||
return &DocumentStore{
|
||||
getDB: getDB,
|
||||
@ -352,7 +352,7 @@ func NewDocumentStore(path string) *DocumentStore {
|
||||
}
|
||||
|
||||
func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore {
|
||||
getDB := NewGetDBFuncFromDB(db, ensureTables)
|
||||
getDB := NewGetDBFuncFromDB(db, ensureDocumentTables)
|
||||
|
||||
return &DocumentStore{
|
||||
getDB: getDB,
|
@ -1,6 +1,7 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
@ -24,5 +25,5 @@ func TestDocumentStore(t *testing.T) {
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewDocumentStore(dsn)
|
||||
|
||||
testsuite.TestDocumentStore(t, store)
|
||||
testsuite.TestDocumentStore(context.Background(), t, store)
|
||||
}
|
75
pkg/storage/driver/sqlite/driver.go
Normal file
75
pkg/storage/driver/sqlite/driver.go
Normal file
@ -0,0 +1,75 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
driver.RegisterDocumentStoreFactory("sqlite", documentStoreFactory)
|
||||
driver.RegisterBlobStoreFactory("sqlite", blobStoreFactory)
|
||||
driver.RegisterShareStoreFactory("sqlite", shareStoreFactory)
|
||||
}
|
||||
|
||||
func documentStoreFactory(url *url.URL) (storage.DocumentStore, error) {
|
||||
dir := filepath.Dir(url.Host + url.Path)
|
||||
|
||||
if dir != ":memory:" {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0750)); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
path := url.Host + url.Path + "?" + url.RawQuery
|
||||
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return NewDocumentStoreWithDB(db), nil
|
||||
}
|
||||
|
||||
func blobStoreFactory(url *url.URL) (storage.BlobStore, error) {
|
||||
dir := filepath.Dir(url.Host + url.Path)
|
||||
|
||||
if dir != ":memory:" {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0750)); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
path := url.Host + url.Path + "?" + url.RawQuery
|
||||
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return NewBlobStoreWithDB(db), nil
|
||||
}
|
||||
|
||||
func shareStoreFactory(url *url.URL) (share.Store, error) {
|
||||
dir := filepath.Dir(url.Host + url.Path)
|
||||
|
||||
if dir != ":memory:" {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0750)); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
path := url.Host + url.Path + "?" + url.RawQuery
|
||||
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return NewShareStoreWithDB(db), nil
|
||||
}
|
428
pkg/storage/driver/sqlite/share_store.go
Normal file
428
pkg/storage/driver/sqlite/share_store.go
Normal file
@ -0,0 +1,428 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type ShareStore struct {
|
||||
getDB GetDBFunc
|
||||
}
|
||||
|
||||
// DeleteAttributes implements share.Repository
|
||||
func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
|
||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
DELETE FROM resources
|
||||
WHERE origin = $1 AND resource_id = $2
|
||||
`
|
||||
args := []any{origin, resourceID}
|
||||
criteria := ""
|
||||
|
||||
for idx, name := range names {
|
||||
if idx == 0 {
|
||||
criteria += " AND ("
|
||||
}
|
||||
|
||||
if idx != 0 {
|
||||
criteria += " OR "
|
||||
}
|
||||
|
||||
criteria += fmt.Sprintf(" name = $%d", len(args)+1)
|
||||
args = append(args, name)
|
||||
|
||||
if idx == len(names)-1 {
|
||||
criteria += " )"
|
||||
}
|
||||
}
|
||||
|
||||
query += criteria
|
||||
|
||||
logger.Debug(
|
||||
ctx, "executing query",
|
||||
logger.F("query", query),
|
||||
logger.F("args", args),
|
||||
)
|
||||
|
||||
res, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
return errors.WithStack(share.ErrNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteResource implements share.Repository
|
||||
func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
|
||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
DELETE FROM resources
|
||||
WHERE origin = $1 AND resource_id = $2
|
||||
`
|
||||
|
||||
args := []any{origin, resourceID}
|
||||
|
||||
logger.Debug(
|
||||
ctx, "executing query",
|
||||
logger.F("query", query),
|
||||
logger.F("args", args),
|
||||
)
|
||||
|
||||
res, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
return errors.WithStack(share.ErrNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FindResources implements share.Repository
|
||||
func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
|
||||
opts := share.NewFindResourcesOptions(funcs...)
|
||||
|
||||
var resources []share.Resource
|
||||
|
||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
SELECT
|
||||
main.origin, main.resource_id,
|
||||
main.name, main.type, main.value,
|
||||
main.created_at, main.updated_at
|
||||
FROM resources AS main
|
||||
JOIN resources AS sub ON
|
||||
main.resource_id = sub.resource_id
|
||||
AND main.origin = sub.origin
|
||||
`
|
||||
|
||||
criteria := " WHERE 1 = 1"
|
||||
preparedArgIndex := 1
|
||||
args := make([]any, 0)
|
||||
|
||||
if opts.Name != nil {
|
||||
criteria += fmt.Sprintf(" AND sub.name = $%d", preparedArgIndex)
|
||||
args = append(args, *opts.Name)
|
||||
preparedArgIndex++
|
||||
}
|
||||
|
||||
if opts.ValueType != nil {
|
||||
criteria += fmt.Sprintf(" AND sub.type = $%d", preparedArgIndex)
|
||||
args = append(args, *opts.ValueType)
|
||||
preparedArgIndex++
|
||||
}
|
||||
|
||||
query += criteria
|
||||
|
||||
logger.Debug(
|
||||
ctx, "executing query",
|
||||
logger.F("query", query),
|
||||
logger.F("args", args),
|
||||
)
|
||||
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
indexedResources := make(map[string]*share.BaseResource)
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
origin string
|
||||
resourceID string
|
||||
name string
|
||||
valueType string
|
||||
value any
|
||||
updatedAt time.Time
|
||||
createdAt time.Time
|
||||
)
|
||||
|
||||
if err := rows.Scan(&origin, &resourceID, &name, &valueType, &value, &createdAt, &updatedAt); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resourceKey := origin + resourceID
|
||||
resource, exists := indexedResources[resourceKey]
|
||||
if !exists {
|
||||
resource = share.NewBaseResource(app.ID(origin), share.ResourceID(resourceID))
|
||||
indexedResources[resourceKey] = resource
|
||||
}
|
||||
|
||||
attr := share.NewBaseAttribute(
|
||||
name,
|
||||
share.ValueType(valueType),
|
||||
value,
|
||||
)
|
||||
|
||||
attr.SetCreatedAt(createdAt)
|
||||
attr.SetUpdatedAt(updatedAt)
|
||||
|
||||
resource.SetAttribute(attr)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources = make([]share.Resource, 0, len(indexedResources))
|
||||
for _, res := range indexedResources {
|
||||
resources = append(resources, res)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// GetResource implements share.Repository
|
||||
func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
|
||||
var (
|
||||
resource *share.BaseResource
|
||||
err error
|
||||
)
|
||||
|
||||
err = s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
resource, err = s.getResourceWithinTx(ctx, tx, origin, resourceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// UpdateAttributes implements share.Repository
|
||||
func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
|
||||
if len(attributes) == 0 {
|
||||
return nil, errors.WithStack(share.ErrAttributeRequired)
|
||||
}
|
||||
|
||||
var resource *share.BaseResource
|
||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
INSERT INTO resources (origin, resource_id, name, type, value, created_at, updated_at)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $6)
|
||||
ON CONFLICT (origin, resource_id, name) DO UPDATE SET
|
||||
type = $4, value = $5, updated_at = $6
|
||||
`
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := stmt.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close statement", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
for _, attr := range attributes {
|
||||
args := []any{
|
||||
string(origin), string(resourceID),
|
||||
attr.Name(), string(attr.Type()), attr.Value(),
|
||||
now, now,
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx, "executing query",
|
||||
logger.F("query", query),
|
||||
logger.F("args", args),
|
||||
)
|
||||
|
||||
if _, err := stmt.ExecContext(ctx, args...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
resource, err = s.getResourceWithinTx(ctx, tx, origin, resourceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
func (s *ShareStore) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin app.ID, resourceID share.ResourceID) (*share.BaseResource, error) {
|
||||
query := `
|
||||
SELECT name, type, value, created_at, updated_at
|
||||
FROM resources
|
||||
WHERE origin = $1 AND resource_id = $2
|
||||
`
|
||||
|
||||
rows, err := tx.QueryContext(ctx, query, origin, resourceID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
attributes := make([]share.Attribute, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
name string
|
||||
valueType string
|
||||
value any
|
||||
updatedAt time.Time
|
||||
createdAt time.Time
|
||||
)
|
||||
|
||||
if err := rows.Scan(&name, &valueType, &value, &createdAt, &updatedAt); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
attr := share.NewBaseAttribute(
|
||||
name,
|
||||
share.ValueType(valueType),
|
||||
value,
|
||||
)
|
||||
|
||||
attr.SetCreatedAt(createdAt)
|
||||
attr.SetUpdatedAt(updatedAt)
|
||||
|
||||
attributes = append(attributes, attr)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(attributes) == 0 {
|
||||
return nil, errors.WithStack(share.ErrNotFound)
|
||||
}
|
||||
|
||||
resource := share.NewBaseResource(origin, resourceID, attributes...)
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
func (s *ShareStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
|
||||
var db *sql.DB
|
||||
|
||||
db, err := s.getDB(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := WithTx(ctx, db, fn); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureShareTables(ctx context.Context, db *sql.DB) error {
|
||||
err := WithTx(ctx, db, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
resource_id TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
value TEXT,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
UNIQUE(origin, resource_id, name) ON CONFLICT REPLACE
|
||||
);
|
||||
`
|
||||
if _, err := tx.ExecContext(ctx, query); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
query = `
|
||||
CREATE INDEX IF NOT EXISTS resource_idx ON resources (origin, resource_id, name);
|
||||
`
|
||||
if _, err := tx.ExecContext(ctx, query); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewShareStore(path string) *ShareStore {
|
||||
getDB := NewGetDBFunc(path, ensureShareTables)
|
||||
|
||||
return &ShareStore{
|
||||
getDB: getDB,
|
||||
}
|
||||
}
|
||||
|
||||
func NewShareStoreWithDB(db *sql.DB) *ShareStore {
|
||||
getDB := NewGetDBFuncFromDB(db, ensureShareTables)
|
||||
|
||||
return &ShareStore{
|
||||
getDB: getDB,
|
||||
}
|
||||
}
|
||||
|
||||
var _ share.Store = &ShareStore{}
|
33
pkg/storage/driver/sqlite/share_store_test.go
Normal file
33
pkg/storage/driver/sqlite/share_store_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestRepository(t *testing.T) {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
testsuite.TestStore(t, newTestStore)
|
||||
}
|
||||
|
||||
func newTestStore(testName string) (share.Store, error) {
|
||||
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
|
||||
file := fmt.Sprintf("./testdata/%s.sqlite", filename)
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewShareStore(dsn)
|
||||
|
||||
return store, nil
|
||||
}
|
1
pkg/storage/driver/sqlite/testdata/.gitignore
vendored
Normal file
1
pkg/storage/driver/sqlite/testdata/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/*.sqlite*
|
@ -8,6 +8,16 @@ func (o *AndOperator) Token() Token {
|
||||
return TokenAnd
|
||||
}
|
||||
|
||||
func (o *AndOperator) AsMap() map[string]any {
|
||||
children := make([]map[string]any, 0, len(o.children))
|
||||
for _, c := range o.children {
|
||||
children = append(children, c.AsMap())
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenAnd): children,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *AndOperator) Children() []Operator {
|
||||
return o.children
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *EqOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *EqOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenEq): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewEqOperator(fields map[string]interface{}) *EqOperator {
|
||||
return &EqOperator{fields}
|
||||
}
|
||||
|
@ -29,6 +29,15 @@ func NewFrom(raw map[string]interface{}) (*Filter, error) {
|
||||
return &Filter{op}, nil
|
||||
}
|
||||
|
||||
func (f *Filter) AsMap() map[string]any {
|
||||
root := f.Root()
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f.Root().AsMap()
|
||||
}
|
||||
|
||||
func toFieldOperator(v interface{}) (Operator, error) {
|
||||
vv, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
@ -93,8 +102,17 @@ func toFieldOperator(v interface{}) (Operator, error) {
|
||||
}
|
||||
|
||||
func toAggregateOperator(token Token, v interface{}) (Operator, error) {
|
||||
vv, ok := v.([]interface{})
|
||||
if !ok {
|
||||
var vv []interface{}
|
||||
|
||||
switch typed := v.(type) {
|
||||
case []interface{}:
|
||||
vv = typed
|
||||
case []map[string]interface{}:
|
||||
vv = make([]interface{}, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
vv = append(vv, item)
|
||||
}
|
||||
default:
|
||||
return nil, errors.WithStack(ErrInvalidAggregationOperator)
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,16 @@ func (o *GtOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *GtOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenGt): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGtOperator(fields OperatorFields) *GtOperator {
|
||||
return &GtOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *GteOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *GteOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenGte): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGteOperator(fields OperatorFields) *GteOperator {
|
||||
return &GteOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *InOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *InOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenIn): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInOperator(fields OperatorFields) *InOperator {
|
||||
return &InOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *LikeOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *LikeOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenLike): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLikeOperator(fields OperatorFields) *LikeOperator {
|
||||
return &LikeOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *LtOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *LtOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenLt): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLtOperator(fields OperatorFields) *LtOperator {
|
||||
return &LtOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *LteOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *LteOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenLte): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLteOperator(fields OperatorFields) *LteOperator {
|
||||
return &LteOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *NeqOperator) Fields() map[string]interface{} {
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *NeqOperator) AsMap() map[string]any {
|
||||
fields := make(map[string]any, len(o.fields))
|
||||
for k, v := range o.fields {
|
||||
fields[k] = v
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenNeq): fields,
|
||||
}
|
||||
}
|
||||
|
||||
func NewNeqOperator(fields map[string]interface{}) *NeqOperator {
|
||||
return &NeqOperator{fields}
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *NotOperator) Children() []Operator {
|
||||
return o.children
|
||||
}
|
||||
|
||||
func (o *NotOperator) AsMap() map[string]any {
|
||||
children := make([]map[string]any, 0, len(o.children))
|
||||
for _, c := range o.children {
|
||||
children = append(children, c.AsMap())
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenNot): children,
|
||||
}
|
||||
}
|
||||
|
||||
func NewNotOperator(ops ...Operator) *NotOperator {
|
||||
return &NotOperator{ops}
|
||||
}
|
||||
|
@ -20,4 +20,15 @@ type OperatorFields map[string]interface{}
|
||||
|
||||
type Operator interface {
|
||||
Token() Token
|
||||
AsMap() map[string]any
|
||||
}
|
||||
|
||||
type FieldOperator interface {
|
||||
Operator
|
||||
Fields() map[string]any
|
||||
}
|
||||
|
||||
type AggregatorOperator interface {
|
||||
Operator
|
||||
Children() []Operator
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ func (o *OrOperator) Children() []Operator {
|
||||
return o.children
|
||||
}
|
||||
|
||||
func (o *OrOperator) AsMap() map[string]any {
|
||||
children := make([]map[string]any, 0, len(o.children))
|
||||
for _, c := range o.children {
|
||||
children = append(children, c.AsMap())
|
||||
}
|
||||
return map[string]any{
|
||||
string(TokenOr): children,
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrOperator(ops ...Operator) *OrOperator {
|
||||
return &OrOperator{ops}
|
||||
}
|
||||
|
8
pkg/storage/share/error.go
Normal file
8
pkg/storage/share/error.go
Normal file
@ -0,0 +1,8 @@
|
||||
package share
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAttributeRequired = errors.New("attribute required")
|
||||
)
|
121
pkg/storage/share/resource.go
Normal file
121
pkg/storage/share/resource.go
Normal file
@ -0,0 +1,121 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
)
|
||||
|
||||
type BaseResource struct {
|
||||
id ResourceID
|
||||
origin app.ID
|
||||
attributes []Attribute
|
||||
}
|
||||
|
||||
// Attributes implements Resource
|
||||
func (r *BaseResource) Attributes() []Attribute {
|
||||
return r.attributes
|
||||
}
|
||||
|
||||
// ID implements Resource
|
||||
func (r *BaseResource) ID() ResourceID {
|
||||
return r.id
|
||||
}
|
||||
|
||||
// Origin implements Resource
|
||||
func (r *BaseResource) Origin() app.ID {
|
||||
return r.origin
|
||||
}
|
||||
|
||||
func (r *BaseResource) SetAttribute(attr Attribute) {
|
||||
for idx, rAttr := range r.attributes {
|
||||
if rAttr.Name() != attr.Name() {
|
||||
continue
|
||||
}
|
||||
|
||||
r.attributes[idx] = attr
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r.attributes = append(r.attributes, attr)
|
||||
}
|
||||
|
||||
func NewBaseResource(origin app.ID, resourceID ResourceID, attributes ...Attribute) *BaseResource {
|
||||
return &BaseResource{
|
||||
id: resourceID,
|
||||
origin: origin,
|
||||
attributes: attributes,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Resource = &BaseResource{}
|
||||
|
||||
type BaseAttribute struct {
|
||||
name string
|
||||
valueType ValueType
|
||||
value any
|
||||
createdAt time.Time
|
||||
updatedAt time.Time
|
||||
}
|
||||
|
||||
// CreatedAt implements Attribute
|
||||
func (a *BaseAttribute) CreatedAt() time.Time {
|
||||
return a.createdAt
|
||||
}
|
||||
|
||||
// Name implements Attribute
|
||||
func (a *BaseAttribute) Name() string {
|
||||
return a.name
|
||||
}
|
||||
|
||||
// Type implements Attribute
|
||||
func (a *BaseAttribute) Type() ValueType {
|
||||
return a.valueType
|
||||
}
|
||||
|
||||
// UpdatedAt implements Attribute
|
||||
func (a *BaseAttribute) UpdatedAt() time.Time {
|
||||
return a.updatedAt
|
||||
}
|
||||
|
||||
// Value implements Attribute
|
||||
func (a *BaseAttribute) Value() any {
|
||||
return a.value
|
||||
}
|
||||
|
||||
func (a *BaseAttribute) SetCreatedAt(createdAt time.Time) {
|
||||
a.createdAt = createdAt
|
||||
}
|
||||
|
||||
func (a *BaseAttribute) SetUpdatedAt(updatedAt time.Time) {
|
||||
a.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
func NewBaseAttribute(name string, valueType ValueType, value any) *BaseAttribute {
|
||||
return &BaseAttribute{
|
||||
name: name,
|
||||
valueType: valueType,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Attribute = &BaseAttribute{}
|
||||
|
||||
func HasAttribute(res Resource, name string, valueType ValueType) bool {
|
||||
return GetAttribute(res, name, valueType) != nil
|
||||
}
|
||||
|
||||
func GetAttribute(res Resource, name string, valueType ValueType) Attribute {
|
||||
for _, attr := range res.Attributes() {
|
||||
if attr.Name() != name {
|
||||
continue
|
||||
}
|
||||
|
||||
if attr.Type() == valueType {
|
||||
return attr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
30
pkg/storage/share/shared_resource_options.go
Normal file
30
pkg/storage/share/shared_resource_options.go
Normal file
@ -0,0 +1,30 @@
|
||||
package share
|
||||
|
||||
type FindResourcesOptionFunc func(*FindResourcesOptions)
|
||||
|
||||
type FindResourcesOptions struct {
|
||||
Name *string
|
||||
ValueType *ValueType
|
||||
}
|
||||
|
||||
func NewFindResourcesOptions(funcs ...FindResourcesOptionFunc) *FindResourcesOptions {
|
||||
opts := &FindResourcesOptions{}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithName(name string) FindResourcesOptionFunc {
|
||||
return func(opts *FindResourcesOptions) {
|
||||
opts.Name = &name
|
||||
}
|
||||
}
|
||||
|
||||
func WithType(valueType ValueType) FindResourcesOptionFunc {
|
||||
return func(opts *FindResourcesOptions) {
|
||||
opts.ValueType = &valueType
|
||||
}
|
||||
}
|
32
pkg/storage/share/shared_resource_store.go
Normal file
32
pkg/storage/share/shared_resource_store.go
Normal file
@ -0,0 +1,32 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
)
|
||||
|
||||
type ResourceID string
|
||||
|
||||
type Resource interface {
|
||||
ID() ResourceID
|
||||
Origin() app.ID
|
||||
Attributes() []Attribute
|
||||
}
|
||||
|
||||
type Attribute interface {
|
||||
Name() string
|
||||
Value() any
|
||||
Type() ValueType
|
||||
UpdatedAt() time.Time
|
||||
CreatedAt() time.Time
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
DeleteResource(ctx context.Context, origin app.ID, resourceID ResourceID) error
|
||||
FindResources(ctx context.Context, funcs ...FindResourcesOptionFunc) ([]Resource, error)
|
||||
GetResource(ctx context.Context, origin app.ID, resourceID ResourceID) (Resource, error)
|
||||
UpdateAttributes(ctx context.Context, origin app.ID, resourceID ResourceID, attributes ...Attribute) (Resource, error)
|
||||
DeleteAttributes(ctx context.Context, origin app.ID, resourceID ResourceID, names ...string) error
|
||||
}
|
15
pkg/storage/share/testsuite/store.go
Normal file
15
pkg/storage/share/testsuite/store.go
Normal file
@ -0,0 +1,15 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
)
|
||||
|
||||
type NewTestStoreFunc func(testname string) (share.Store, error)
|
||||
|
||||
func TestStore(t *testing.T, newStore NewTestStoreFunc) {
|
||||
t.Run("Cases", func(t *testing.T) {
|
||||
runRepositoryTests(t, newStore)
|
||||
})
|
||||
}
|
342
pkg/storage/share/testsuite/store_cases.go
Normal file
342
pkg/storage/share/testsuite/store_cases.go
Normal file
@ -0,0 +1,342 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type repositoryTestCase struct {
|
||||
Name string
|
||||
Skip bool
|
||||
Run func(ctx context.Context, t *testing.T, store share.Store) error
|
||||
}
|
||||
|
||||
var repositoryTestCases = []repositoryTestCase{
|
||||
{
|
||||
Name: "Update resource attributes",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
origin := app.ID("test")
|
||||
resourceID := share.ResourceID("test")
|
||||
|
||||
// Try to create resource without attributes
|
||||
_, err := store.UpdateAttributes(ctx, origin, resourceID)
|
||||
if err == nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, share.ErrAttributeRequired) {
|
||||
return errors.Errorf("err: expected share.ErrAttributeRequired, got '%v'", err)
|
||||
}
|
||||
|
||||
attributes := []share.Attribute{
|
||||
share.NewBaseAttribute("my_text_attr", share.TypeText, "foo"),
|
||||
share.NewBaseAttribute("my_number_attr", share.TypeNumber, 5),
|
||||
share.NewBaseAttribute("my_path_attr", share.TypePath, "/my/path"),
|
||||
share.NewBaseAttribute("my_bool_attr", share.TypeBool, true),
|
||||
}
|
||||
|
||||
resource, err := store.UpdateAttributes(ctx, origin, resourceID, attributes...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
isNil := reflect.ValueOf(resource).IsNil()
|
||||
if isNil {
|
||||
return errors.New("resource should not be nil")
|
||||
}
|
||||
|
||||
if e, g := resourceID, resource.ID(); e != g {
|
||||
return errors.Errorf("resource.ID(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := origin, resource.Origin(); e != g {
|
||||
return errors.Errorf("resource.Origin(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := 4, len(resource.Attributes()); e != g {
|
||||
return errors.Errorf("len(resource.Attributes()): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Find resources by attribute name",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_name.json", store); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources, err := store.FindResources(ctx, share.WithName("my_number"))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
isNil := reflect.ValueOf(resources).IsNil()
|
||||
if isNil {
|
||||
return errors.New("resources should not be nil")
|
||||
}
|
||||
|
||||
if e, g := 2, len(resources); e != g {
|
||||
return errors.Errorf("len(resources): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Find resources by attribute type",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type.json", store); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources, err := store.FindResources(ctx, share.WithType(share.TypePath))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
isNil := reflect.ValueOf(resources).IsNil()
|
||||
if isNil {
|
||||
return errors.New("resources should not be nil")
|
||||
}
|
||||
|
||||
if e, g := 1, len(resources); e != g {
|
||||
return errors.Errorf("len(resources): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Find resources by attribute type and name",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type_and_name.json", store); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources, err := store.FindResources(ctx, share.WithType(share.TypeText), share.WithName("my_attr"))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
isNil := reflect.ValueOf(resources).IsNil()
|
||||
if isNil {
|
||||
return errors.New("resources should not be nil")
|
||||
}
|
||||
|
||||
if e, g := 1, len(resources); e != g {
|
||||
return errors.Errorf("len(resources): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Get resource",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
if err := loadTestData(ctx, "testdata/get_resource.json", store); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
origin := app.ID("app1.edge.app")
|
||||
resourceID := share.ResourceID("res-1")
|
||||
|
||||
resource, err := store.GetResource(ctx, origin, resourceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
isNil := reflect.ValueOf(resource).IsNil()
|
||||
if isNil {
|
||||
return errors.New("resources should not be nil")
|
||||
}
|
||||
|
||||
if e, g := origin, resource.Origin(); e != g {
|
||||
return errors.Errorf("resource.Origin(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := resourceID, resource.ID(); e != g {
|
||||
return errors.Errorf("resource.ID(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
resource, err = store.GetResource(ctx, origin, "unexistant-id")
|
||||
if err == nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, share.ErrNotFound) {
|
||||
return errors.Errorf("err: expected share.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Delete resource",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
if err := loadTestData(ctx, "testdata/delete_resource.json", store); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
origin := app.ID("app1.edge.app")
|
||||
resourceID := share.ResourceID("res-1")
|
||||
|
||||
// It should delete an existing resource
|
||||
if err := store.DeleteResource(ctx, origin, resourceID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err := store.GetResource(ctx, origin, resourceID)
|
||||
if err == nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
// The resource should be deleted
|
||||
if !errors.Is(err, share.ErrNotFound) {
|
||||
return errors.Errorf("err: expected share.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
// It should not delete an unexistant resource
|
||||
err = store.DeleteResource(ctx, origin, resourceID)
|
||||
if err == nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, share.ErrNotFound) {
|
||||
return errors.Errorf("err: expected share.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
otherOrigin := app.ID("app2.edge.app")
|
||||
|
||||
// It should not delete a resource with the same id and another origin
|
||||
resource, err := store.GetResource(ctx, otherOrigin, resourceID)
|
||||
if err != nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
if e, g := otherOrigin, resource.Origin(); e != g {
|
||||
return errors.Errorf("resource.Origin(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Delete attributes",
|
||||
Skip: false,
|
||||
Run: func(ctx context.Context, t *testing.T, store share.Store) error {
|
||||
if err := loadTestData(ctx, "testdata/delete_attributes.json", store); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
origin := app.ID("app1.edge.app")
|
||||
resourceID := share.ResourceID("res-1")
|
||||
|
||||
// It should delete specified attributes
|
||||
if err := store.DeleteAttributes(ctx, origin, resourceID, "my_text", "my_bool"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resource, err := store.GetResource(ctx, origin, resourceID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 1, len(resource.Attributes()); e != g {
|
||||
return errors.Errorf("len(resource.Attributes()): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
attr := share.GetAttribute(resource, "my_number", share.TypeNumber)
|
||||
if attr == nil {
|
||||
return errors.New("attr shoudl not be nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runRepositoryTests(t *testing.T, newRepo NewTestStoreFunc) {
|
||||
for _, tc := range repositoryTestCases {
|
||||
func(tc repositoryTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if tc.Skip {
|
||||
t.SkipNow()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
repo, err := newRepo(tc.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := tc.Run(ctx, t, repo); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
||||
|
||||
type jsonResource struct {
|
||||
ID string `json:"id"`
|
||||
Origin string `json:"origin"`
|
||||
Attributes []jsonAttribute `json:"attributes"`
|
||||
}
|
||||
|
||||
type jsonAttribute struct {
|
||||
Name string `json:"name"`
|
||||
Type share.ValueType `json:"type"`
|
||||
Value any `json:"value"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func loadTestData(ctx context.Context, jsonFile string, store share.Store) error {
|
||||
data, err := testData.ReadFile(jsonFile)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var resources []jsonResource
|
||||
|
||||
if err := json.Unmarshal(data, &resources); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, res := range resources {
|
||||
attributes := make([]share.Attribute, len(res.Attributes))
|
||||
|
||||
for idx, attr := range res.Attributes {
|
||||
attributes[idx] = share.NewBaseAttribute(
|
||||
attr.Name,
|
||||
attr.Type,
|
||||
attr.Value,
|
||||
)
|
||||
}
|
||||
|
||||
_, err := store.UpdateAttributes(ctx, app.ID(res.Origin), share.ResourceID(res.ID), attributes...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
6
pkg/storage/share/testsuite/testdata.go
Normal file
6
pkg/storage/share/testsuite/testdata.go
Normal file
@ -0,0 +1,6 @@
|
||||
package testsuite
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed testdata/*
|
||||
var testData embed.FS
|
11
pkg/storage/share/testsuite/testdata/delete_attributes.json
vendored
Normal file
11
pkg/storage/share/testsuite/testdata/delete_attributes.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" },
|
||||
{ "name":"my_bool", "type": "bool", "value": true },
|
||||
{ "name":"my_number", "type": "number", "value": 5 }
|
||||
]
|
||||
}
|
||||
]
|
16
pkg/storage/share/testsuite/testdata/delete_resource.json
vendored
Normal file
16
pkg/storage/share/testsuite/testdata/delete_resource.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app2.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "foo" }
|
||||
]
|
||||
}
|
||||
]
|
29
pkg/storage/share/testsuite/testdata/find_resources_by_attribute_name.json
vendored
Normal file
29
pkg/storage/share/testsuite/testdata/find_resources_by_attribute_name.json
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" },
|
||||
{ "name":"my_number", "type": "number", "value": 5 },
|
||||
{ "name":"my_bool", "type": "bool", "value": true },
|
||||
{ "name":"my_path", "type": "path", "value": "/my/icon.png" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-2",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"other_text", "type": "text", "value": "foo" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app2.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" },
|
||||
{ "name":"my_number", "type": "number", "value": 5 },
|
||||
{ "name":"my_bool", "type": "bool", "value": true },
|
||||
{ "name":"my_path", "type": "path", "value": "/my/icon.png" }
|
||||
]
|
||||
}
|
||||
]
|
28
pkg/storage/share/testsuite/testdata/find_resources_by_attribute_type.json
vendored
Normal file
28
pkg/storage/share/testsuite/testdata/find_resources_by_attribute_type.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" },
|
||||
{ "name":"my_number", "type": "number", "value": 5 },
|
||||
{ "name":"my_bool", "type": "bool", "value": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-2",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"other_text", "type": "text", "value": "foo" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app2.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" },
|
||||
{ "name":"my_number", "type": "number", "value": 5 },
|
||||
{ "name":"my_bool", "type": "bool", "value": true },
|
||||
{ "name":"my_path", "type": "path", "value": "/my/icon.png" }
|
||||
]
|
||||
}
|
||||
]
|
23
pkg/storage/share/testsuite/testdata/find_resources_by_attribute_type_and_name.json
vendored
Normal file
23
pkg/storage/share/testsuite/testdata/find_resources_by_attribute_type_and_name.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_attr", "type": "text", "value": "bar" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-2",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_attr", "type": "bool", "value": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app2.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_attr", "type": "number", "value": 5 }
|
||||
]
|
||||
}
|
||||
]
|
16
pkg/storage/share/testsuite/testdata/get_resource.json
vendored
Normal file
16
pkg/storage/share/testsuite/testdata/get_resource.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app1.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "bar" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "res-1",
|
||||
"origin": "app2.edge.app",
|
||||
"attributes": [
|
||||
{ "name":"my_text", "type": "text", "value": "foo" }
|
||||
]
|
||||
}
|
||||
]
|
86
pkg/storage/share/value_type.go
Normal file
86
pkg/storage/share/value_type.go
Normal file
@ -0,0 +1,86 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ValueType string
|
||||
|
||||
const (
|
||||
TypeText ValueType = "text"
|
||||
TypePath ValueType = "path"
|
||||
TypeNumber ValueType = "number"
|
||||
TypeBool ValueType = "bool"
|
||||
)
|
||||
|
||||
func AssertType(value any, valueType ValueType) error {
|
||||
switch valueType {
|
||||
case TypeText:
|
||||
if err := AssertTypeText(value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
case TypeNumber:
|
||||
if err := AssertTypeNumber(value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
case TypeBool:
|
||||
if err := AssertTypeBool(value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
case TypePath:
|
||||
if err := AssertTypePath(value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.Errorf("value type '%s' does not exist", valueType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AssertTypeText(value any) error {
|
||||
_, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.Errorf("invalid value for type '%s': '%v'", TypeText, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AssertTypeNumber(value any) error {
|
||||
switch value.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return nil
|
||||
|
||||
default:
|
||||
return errors.Errorf("invalid value for type '%s': '%v'", TypeNumber, value)
|
||||
}
|
||||
}
|
||||
|
||||
func AssertTypeBool(value any) error {
|
||||
_, ok := value.(bool)
|
||||
if !ok {
|
||||
return errors.Errorf("invalid value for type '%s': '%v'", TypeBool, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AssertTypePath(value any) error {
|
||||
path, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.Errorf("invalid value for type '%s': '%v'", TypePath, value)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return errors.Errorf("value '%s' should start with a '/'", value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
88
pkg/storage/share/value_type_test.go
Normal file
88
pkg/storage/share/value_type_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type valueTypeTestCase struct {
|
||||
Name string
|
||||
Value any
|
||||
Type ValueType
|
||||
ShouldFail bool
|
||||
}
|
||||
|
||||
var valueTypeTestCases = []valueTypeTestCase{
|
||||
{
|
||||
Name: "Valid text",
|
||||
Value: "my_text",
|
||||
Type: TypeText,
|
||||
},
|
||||
{
|
||||
Name: "Invalid text",
|
||||
Value: 0,
|
||||
Type: TypeText,
|
||||
ShouldFail: true,
|
||||
},
|
||||
{
|
||||
Name: "Valid number",
|
||||
Value: 5.6,
|
||||
Type: TypeNumber,
|
||||
},
|
||||
{
|
||||
Name: "Invalid number",
|
||||
Value: "5",
|
||||
Type: TypeNumber,
|
||||
ShouldFail: true,
|
||||
},
|
||||
{
|
||||
Name: "Valid bool",
|
||||
Value: false,
|
||||
Type: TypeBool,
|
||||
},
|
||||
{
|
||||
Name: "Invalid bool",
|
||||
Value: "yes",
|
||||
Type: TypeBool,
|
||||
ShouldFail: true,
|
||||
},
|
||||
{
|
||||
Name: "Valid path",
|
||||
Value: "/foo/bar",
|
||||
Type: TypePath,
|
||||
},
|
||||
{
|
||||
Name: "Invalid path",
|
||||
Value: true,
|
||||
Type: TypePath,
|
||||
ShouldFail: true,
|
||||
},
|
||||
{
|
||||
Name: "Missing slash",
|
||||
Value: "missing/slash",
|
||||
Type: TypePath,
|
||||
ShouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAssertType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range valueTypeTestCases {
|
||||
func(tc valueTypeTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := AssertType(tc.Value, tc.Type)
|
||||
if tc.ShouldFail && err == nil {
|
||||
t.Errorf("err should not be nil")
|
||||
}
|
||||
|
||||
if !tc.ShouldFail && err != nil {
|
||||
t.Errorf("err: expected nil, got '%+v'", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestBlobStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
file := "./testdata/blobstore_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewBlobStore(dsn)
|
||||
|
||||
testsuite.TestBlobStore(t, store)
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
)
|
||||
|
||||
func TestBlobStore(t *testing.T, store storage.BlobStore) {
|
||||
func TestBlobStore(ctx context.Context, t *testing.T, store storage.BlobStore) {
|
||||
t.Run("Ops", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testBlobStoreOps(t, store)
|
||||
testBlobStoreOps(ctx, t, store)
|
||||
})
|
||||
}
|
||||
|
79
pkg/storage/testsuite/blob_store_benchmark.go
Normal file
79
pkg/storage/testsuite/blob_store_benchmark.go
Normal file
@ -0,0 +1,79 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func BenchmarkBlobStore(t *testing.B, store storage.BlobStore) {
|
||||
t.Run("BlobCreateUpdateReadDelete", func(t *testing.B) {
|
||||
|
||||
for i := 0; i < t.N; i++ {
|
||||
bucketName := fmt.Sprintf("bucket-%d", i)
|
||||
if err := runBlobCreateUpdateReadDelete(store, bucketName); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func runBlobCreateUpdateReadDelete(store storage.BlobStore, bucketName string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
bucket, err := store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blobID := storage.NewBlobID()
|
||||
|
||||
writer, err := bucket.NewWriter(ctx, blobID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
data := []byte("foo")
|
||||
|
||||
if _, err = writer.Write(data); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
reader, err := bucket.NewReader(ctx, blobID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if _, err = io.Copy(&buf, reader); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := reader.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := bucket.Delete(ctx, blobID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := bucket.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := store.DeleteBucket(ctx, bucketName); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -17,9 +17,11 @@ type blobStoreTestCase struct {
|
||||
|
||||
var blobStoreTestCases = []blobStoreTestCase{
|
||||
{
|
||||
Name: "Open new bucket",
|
||||
Name: "Open then delete bucket",
|
||||
Run: func(ctx context.Context, store storage.BlobStore) error {
|
||||
bucket, err := store.OpenBucket(ctx, "open-new-bucket")
|
||||
bucketName := "open-new-bucket"
|
||||
|
||||
bucket, err := store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -50,6 +52,23 @@ var blobStoreTestCases = []blobStoreTestCase{
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := store.DeleteBucket(ctx, bucketName); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
buckets, err := store.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, b := range buckets {
|
||||
if b != bucketName {
|
||||
continue
|
||||
}
|
||||
|
||||
return errors.Errorf("bucket '%s' should be deleted", bucketName)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@ -112,14 +131,12 @@ var blobStoreTestCases = []blobStoreTestCase{
|
||||
},
|
||||
}
|
||||
|
||||
func testBlobStoreOps(t *testing.T, store storage.BlobStore) {
|
||||
func testBlobStoreOps(ctx context.Context, t *testing.T, store storage.BlobStore) {
|
||||
for _, tc := range blobStoreTestCases {
|
||||
func(tc blobStoreTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err := tc.Run(ctx, store); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
)
|
||||
|
||||
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
|
||||
func TestDocumentStore(ctx context.Context, t *testing.T, store storage.DocumentStore) {
|
||||
t.Run("Ops", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testDocumentStoreOps(t, store)
|
||||
testDocumentStoreOps(ctx, t, store)
|
||||
})
|
||||
}
|
||||
|
@ -433,12 +433,12 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||
},
|
||||
}
|
||||
|
||||
func testDocumentStoreOps(t *testing.T, store storage.DocumentStore) {
|
||||
func testDocumentStoreOps(ctx context.Context, t *testing.T, store storage.DocumentStore) {
|
||||
for _, tc := range documentStoreOpsTestCases {
|
||||
func(tc documentStoreOpsTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tc.Run(context.Background(), store); err != nil {
|
||||
if err := tc.Run(ctx, store); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
|
Reference in New Issue
Block a user