Skip to content
This repository has been archived by the owner on Apr 5, 2023. It is now read-only.

Commit

Permalink
feat: Implement CouchDB Store
Browse files Browse the repository at this point in the history
Signed-off-by: Derek Trider <[email protected]>
  • Loading branch information
Derek Trider committed Feb 19, 2020
1 parent 2f206aa commit 42630ee
Show file tree
Hide file tree
Showing 8 changed files with 688 additions and 56 deletions.
14 changes: 13 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,16 @@ module github.com/trustbloc/edge-core

go 1.13

require github.com/stretchr/testify v1.4.0
require (
github.com/flimzy/diff v0.1.6 // indirect
github.com/flimzy/testy v0.1.16 // indirect
github.com/go-kivik/couchdb v2.0.0+incompatible
github.com/go-kivik/kivik v2.0.0+incompatible
github.com/go-kivik/kiviktest v2.0.0+incompatible // indirect
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
gitlab.com/flimzy/testy v0.0.2 // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
)
40 changes: 40 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,50 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flimzy/diff v0.1.6 h1:ufTsTKcDtlaczpJTo3u1NeYqzuP6oRpy1VwQUIrgmBY=
github.com/flimzy/diff v0.1.6/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
github.com/flimzy/testy v0.1.16 h1:nchF7XYCkfHJiZKMRhAVKQp8jzpXFPwJYnSrnFysqlI=
github.com/flimzy/testy v0.1.16/go.mod h1:3szguN8NXqgq9bt9Gu8TQVj698PJWmyx/VY1frwwKrM=
github.com/go-kivik/couchdb v2.0.0+incompatible h1:DsXVuGJTng04Guz8tg7jGVQ53RlByEhk+gPB/1yo3Oo=
github.com/go-kivik/couchdb v2.0.0+incompatible/go.mod h1:5XJRkAMpBlEVA4q0ktIZjUPYBjoBmRoiWvwUBzP3BOQ=
github.com/go-kivik/kivik v2.0.0+incompatible h1:/7hgr29DKv/vlaJsUoyRlOFq0K+3ikz0wTbu+cIs7QY=
github.com/go-kivik/kivik v2.0.0+incompatible/go.mod h1:nIuJ8z4ikBrVUSk3Ua8NoDqYKULPNjuddjqRvlSUyyQ=
github.com/go-kivik/kiviktest v2.0.0+incompatible h1:y1RyPHqWQr+eFlevD30Tr3ipiPCxK78vRoD3o9YysjI=
github.com/go-kivik/kiviktest v2.0.0+incompatible/go.mod h1:JdhVyzixoYhoIDUt6hRf1yAfYyaDa5/u9SDOindDkfQ=
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d h1:vr95xIx8Eg3vCzZPxY3rCwTfkjqNDt/FgVqTOk0WByk=
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc=
github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gitlab.com/flimzy/testy v0.0.2 h1:wii65HpZEbstqGT44+msiwzrX7SaxqNXj0BFjJc9iUY=
gitlab.com/flimzy/testy v0.0.2/go.mod h1:YObF4cq711ubd/3U0ydRQQVz7Cnq/ChgJpVwNr/AJac=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
Expand Down
204 changes: 204 additions & 0 deletions pkg/storage/couchdb/couchdbstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package couchdbstore

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"sync"

_ "github.com/go-kivik/couchdb" // The CouchDB driver
"github.com/go-kivik/kivik"

"github.com/trustbloc/edge-core/pkg/storage"
)

// Provider represents an CouchDB implementation of the storage.Provider interface
type Provider struct {
hostURL string
couchDBClient *kivik.Client
dbs map[string]*CouchDBStore
mux sync.RWMutex
}

const (
blankHostErrMsg = "hostURL for new CouchDB provider can't be blank"
failToCloseProviderErrMsg = "failed to close provider"
)

// NewProvider instantiates Provider
func NewProvider(hostURL string) (*Provider, error) {
if hostURL == "" {
return nil, errors.New(blankHostErrMsg)
}

client, err := kivik.New("couch", hostURL)
if err != nil {
return nil, err
}

return &Provider{hostURL: hostURL, couchDBClient: client, dbs: map[string]*CouchDBStore{}}, nil
}

// CreateStore creates a new store with the given name.
func (p *Provider) CreateStore(name string) error {
p.mux.Lock()
defer p.mux.Unlock()

err := p.couchDBClient.CreateDB(context.Background(), name)

return err
}

// OpenStore opens an existing store with the given name and returns it.
func (p *Provider) OpenStore(name string) (storage.Store, error) {
p.mux.Lock()
defer p.mux.Unlock()

// Check cache first
cachedStore, existsInCache := p.dbs[name]
if existsInCache {
return cachedStore, nil
}

// If it's not in the cache, then let's ask the CouchDB server if it exists
existsOnServer, err := p.couchDBClient.DBExists(context.Background(), name)
if err != nil {
return nil, err
}

if !existsOnServer {
return nil, storage.ErrStoreNotFound
}

db := p.couchDBClient.DB(context.Background(), name)

// db.Err() won't return an error if the database doesn't exist, hence the need for the explicit DBExists call above
if dbErr := db.Err(); dbErr != nil {
return nil, dbErr
}

store := &CouchDBStore{db: db}

p.dbs[name] = store

return store, nil
}

// CloseStore closes a previously opened store.
func (p *Provider) CloseStore(name string) error {
p.mux.Lock()
defer p.mux.Unlock()

store, exists := p.dbs[name]
if !exists {
return storage.ErrStoreNotFound
}

delete(p.dbs, name)

return store.db.Close(context.Background())
}

// Close closes the provider.
func (p *Provider) Close() error {
p.mux.Lock()
defer p.mux.Unlock()

for _, store := range p.dbs {
err := store.db.Close(context.Background())
if err != nil {
return fmt.Errorf(failToCloseProviderErrMsg+": %w", err)
}
}

return p.couchDBClient.Close(context.Background())
}

// CouchDBStore represents a CouchDB-backed database.
type CouchDBStore struct {
db *kivik.DB
}

// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
var valueToPut []byte
if isJSON(v) {
valueToPut = v
} else {
valueToPut = wrapTextAsCouchDBAttachment(v)
}

_, err := c.db.Put(context.Background(), k, valueToPut)
if err != nil {
return err
}

return nil
}

func isJSON(textToCheck []byte) bool {
var js json.RawMessage
return json.Unmarshal(textToCheck, &js) == nil
}

// Kivik has a PutAttachment method, but it requires creating a document first and then adding an attachment after.
// We want to do it all in one step, hence this manual stuff below.
func wrapTextAsCouchDBAttachment(textToWrap []byte) []byte {
encodedTextToWrap := base64.StdEncoding.EncodeToString(textToWrap)
return []byte(`{"_attachments": {"data": {"data": "` + encodedTextToWrap + `", "content_type": "text/plain"}}}`)
}

// Get retrieves the value in the store associated with the given key.
func (c *CouchDBStore) Get(k string) ([]byte, error) {
destinationData := make(map[string]interface{})

row := c.db.Get(context.Background(), k)

err := row.ScanDoc(&destinationData)
if err != nil {
if err.Error() == "Not Found: missing" {
return nil, storage.ErrValueNotFound
}

return nil, err
}

_, containsAttachment := destinationData["_attachments"]
if containsAttachment {
return c.getDataFromAttachment(k)
}

// Stripping out the CouchDB-specific fields
delete(destinationData, "_id")
delete(destinationData, "_rev")

strippedJSON, err := json.Marshal(destinationData)
if err != nil {
return nil, err
}

return strippedJSON, nil
}

func (c *CouchDBStore) getDataFromAttachment(k string) ([]byte, error) {
// Original data was not JSON and so it was stored as an attachment
attachment, err := c.db.GetAttachment(context.Background(), k, "data")
if err != nil {
return nil, err
}

data, err := ioutil.ReadAll(attachment.Content)
if err != nil {
return nil, err
}

return data, nil
}
Loading

0 comments on commit 42630ee

Please sign in to comment.