From 8bd65f84fc81d2ed53b3ceafc928051a4d707fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20Ortu=C3=B1o?= Date: Wed, 2 Mar 2022 21:01:26 +0100 Subject: [PATCH] added BoltDB repository (#212) * added BoltDB repository * updated CHANGELOG.md --- .gitignore | 3 + CHANGELOG.md | 2 + README.md | 2 +- config/example.config.yaml | 51 ++- go.mod | 8 +- go.sum | 8 +- pkg/admin/server/service.go | 5 +- pkg/storage/boltdb/blocklist.go | 113 +++++++ pkg/storage/boltdb/blocklist_test.go | 130 ++++++++ pkg/storage/boltdb/capabilities.go | 99 ++++++ pkg/storage/boltdb/capabilities_test.go | 73 +++++ pkg/storage/boltdb/last.go | 97 ++++++ pkg/storage/boltdb/last_test.go | 77 +++++ pkg/storage/boltdb/locker.go | 34 ++ pkg/storage/boltdb/offline.go | 118 +++++++ pkg/storage/boltdb/offline_test.go | 113 +++++++ pkg/storage/boltdb/op.go | 159 +++++++++ pkg/storage/boltdb/private.go | 95 ++++++ pkg/storage/boltdb/private_test.go | 73 +++++ pkg/storage/boltdb/repository.go | 93 ++++++ pkg/storage/boltdb/roster.go | 409 ++++++++++++++++++++++++ pkg/storage/boltdb/roster_test.go | 187 +++++++++++ pkg/storage/boltdb/tx.go | 46 +++ pkg/storage/boltdb/user.go | 114 +++++++ pkg/storage/boltdb/user_test.go | 96 ++++++ pkg/storage/boltdb/util_test.go | 55 ++++ pkg/storage/boltdb/vcard.go | 97 ++++++ pkg/storage/boltdb/vcard_test.go | 73 +++++ pkg/storage/storage.go | 26 +- 29 files changed, 2415 insertions(+), 41 deletions(-) create mode 100644 pkg/storage/boltdb/blocklist.go create mode 100644 pkg/storage/boltdb/blocklist_test.go create mode 100644 pkg/storage/boltdb/capabilities.go create mode 100644 pkg/storage/boltdb/capabilities_test.go create mode 100644 pkg/storage/boltdb/last.go create mode 100644 pkg/storage/boltdb/last_test.go create mode 100644 pkg/storage/boltdb/locker.go create mode 100644 pkg/storage/boltdb/offline.go create mode 100644 pkg/storage/boltdb/offline_test.go create mode 100644 pkg/storage/boltdb/op.go create mode 100644 pkg/storage/boltdb/private.go create mode 100644 pkg/storage/boltdb/private_test.go create mode 100644 pkg/storage/boltdb/repository.go create mode 100644 pkg/storage/boltdb/roster.go create mode 100644 pkg/storage/boltdb/roster_test.go create mode 100644 pkg/storage/boltdb/tx.go create mode 100644 pkg/storage/boltdb/user.go create mode 100644 pkg/storage/boltdb/user_test.go create mode 100644 pkg/storage/boltdb/util_test.go create mode 100644 pkg/storage/boltdb/vcard.go create mode 100644 pkg/storage/boltdb/vcard_test.go diff --git a/.gitignore b/.gitignore index d8e1e69d0..8d1da2a24 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ build # Mock files *.mock_test.go + +# BoltDB default database file +.jackal.db diff --git a/CHANGELOG.md b/CHANGELOG.md index 607680f4c..e57fd639c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## jackal - main / unreleased +* [FEATURE] Added BoltDB repository type. [#198](https://github.com/ortuman/jackal/pull/212) + ## 0.57.0 (2022/02/12) * [ENHANCEMENT] Added memory ballast. [#198](https://github.com/ortuman/jackal/pull/198) diff --git a/README.md b/README.md index 4f8fe9923..438249efd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ jackal supports the following features: - Customizable - Enforced SSL/TLS - Stream compression (zlib) -- Database connectivity for storing offline messages and user settings (PostgreSQL 9.5+) +- Database connectivity for storing offline messages and user settings (PostgreSQL 9.5+, BoltDB) - Caching (Redis 6.2+) - Clustering capabilities (etcd 3.4+) - Expose [prometheus](https://prometheus.io/) metrics diff --git a/config/example.config.yaml b/config/example.config.yaml index 90bcbb269..5197eaa1e 100644 --- a/config/example.config.yaml +++ b/config/example.config.yaml @@ -23,31 +23,31 @@ peppers: # cert_file: "" # privkey_file: "" -storage: - type: pgsql - pgsql: - host: 127.0.0.1:5432 - user: jackal - password: password - database: jackal - max_open_conns: 16 - - cache: - type: redis - redis: - addresses: - - localhost:6379 - -cluster: - type: kv - kv: - type: etcd - etcd: - endpoints: - - http://127.0.0.1:2379 - - server: - port: 14369 +#storage: +# type: pgsql +# pgsql: +# host: 127.0.0.1:5432 +# user: jackal +# password: password +# database: jackal +# max_open_conns: 16 + +# cache: +# type: redis +# redis: +# addresses: +# - localhost:6379 + +#cluster: +# type: kv +# kv: +# type: etcd +# etcd: +# endpoints: +# - http://127.0.0.1:2379 +# +# server: +# port: 14369 shapers: - name: super @@ -136,4 +136,3 @@ components: listeners: - port: 5275 secret: hqcUrfHtgE73FktcXwfrP - diff --git a/go.mod b/go.mod index 1e513f19d..1ab0902dd 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/uuid v1.1.2 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/jackal-xmpp/runqueue/v2 v2.0.0 - github.com/jackal-xmpp/stravaganza v1.1.1 + github.com/jackal-xmpp/stravaganza v1.2.1 github.com/kkyr/fig v0.2.0 github.com/lib/pq v1.8.0 github.com/mattn/go-sqlite3 v1.14.5 // indirect @@ -24,6 +24,7 @@ require ( github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 + go.etcd.io/bbolt v1.3.5 go.etcd.io/etcd/client/v3 v3.5.1 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba @@ -31,8 +32,6 @@ require ( google.golang.org/protobuf v1.26.0 ) -require github.com/gogo/protobuf v1.3.2 - require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect @@ -42,6 +41,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect @@ -69,3 +69,5 @@ require ( ) replace go.etcd.io/etcd/v3 => github.com/etcd-io/etcd/v3 v3.5.1 + +replace go.etcd.io/bbolt => github.com/etcd-io/bbolt v1.3.5 diff --git a/go.sum b/go.sum index 5336d2e72..327ca978c 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/etcd-io/bbolt v1.3.5 h1:3Uslx5o2Ds0IBbRW/L7/kiV5oR/kcG85nSnLsvEdKQI= +github.com/etcd-io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -223,8 +225,8 @@ github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62 github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackal-xmpp/runqueue/v2 v2.0.0 h1:QfvOfL6zF5yK1LN5TKabpj+VBuELMwtR8Xpkz0CrjoI= github.com/jackal-xmpp/runqueue/v2 v2.0.0/go.mod h1:tXZARVqBMGeV8BTc/qDPg0qXILTUWmER7wlYbN9Xcac= -github.com/jackal-xmpp/stravaganza v1.1.1 h1:P7mpUNc+B5d8TF+PErmiK9ayCo+AC+xIxQBEV1dD6Eo= -github.com/jackal-xmpp/stravaganza v1.1.1/go.mod h1:C2sH3I3kQQWsOs6+Cg+il0mzGPLnyy+QgVwTxbyJ5kk= +github.com/jackal-xmpp/stravaganza v1.2.1 h1:FDCWRhBmrKAxIBr4ix7Bugd557f18Lg+lTmPWom0g9A= +github.com/jackal-xmpp/stravaganza v1.2.1/go.mod h1:C2sH3I3kQQWsOs6+Cg+il0mzGPLnyy+QgVwTxbyJ5kk= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -414,7 +416,6 @@ github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd/api/v3 v3.5.1 h1:v28cktvBq+7vGyJXF8G+rWJmj+1XUmMtqcLnH8hDocM= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1 h1:XIQcHCFSG53bJETYeRJtIxdLv2EWRGxcfzR8lSnTH4E= @@ -529,6 +530,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/admin/server/service.go b/pkg/admin/server/service.go index 2aa0bcc9d..b71b4d7b9 100644 --- a/pkg/admin/server/service.go +++ b/pkg/admin/server/service.go @@ -154,7 +154,10 @@ func (s *usersService) upsertUser(ctx context.Context, username, password string hSHA512 := hashPassword([]byte(password), pepperedSalt, iterationCount, sha512.Size, sha512.New) hSHA3512 := hashPassword([]byte(password), pepperedSalt, iterationCount, sha512.Size, sha3.New512) - usr := usermodel.User{Username: username} + usr := usermodel.User{ + Username: username, + Scram: &usermodel.Scram{}, + } usr.Scram.Sha1 = base64.RawURLEncoding.EncodeToString(hSHA1) usr.Scram.Sha256 = base64.RawURLEncoding.EncodeToString(hSHA256) usr.Scram.Sha512 = base64.RawURLEncoding.EncodeToString(hSHA512) diff --git a/pkg/storage/boltdb/blocklist.go b/pkg/storage/boltdb/blocklist.go new file mode 100644 index 000000000..ce8d32663 --- /dev/null +++ b/pkg/storage/boltdb/blocklist.go @@ -0,0 +1,113 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + blocklistmodel "github.com/ortuman/jackal/pkg/model/blocklist" + bolt "go.etcd.io/bbolt" +) + +type boltDBBlockListRep struct { + tx *bolt.Tx +} + +func newBlockListRep(tx *bolt.Tx) *boltDBBlockListRep { + return &boltDBBlockListRep{tx: tx} +} + +func (r *boltDBBlockListRep) UpsertBlockListItem(_ context.Context, item *blocklistmodel.Item) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: blockListBucket(item.Username), + key: item.Jid, + obj: item, + } + return op.do() +} + +func (r *boltDBBlockListRep) DeleteBlockListItem(_ context.Context, item *blocklistmodel.Item) error { + op := delKeyOp{ + tx: r.tx, + bucket: blockListBucket(item.Username), + key: item.Jid, + } + return op.do() +} + +func (r *boltDBBlockListRep) FetchBlockListItems(_ context.Context, username string) ([]*blocklistmodel.Item, error) { + var retVal []*blocklistmodel.Item + + op := iterKeysOp{ + tx: r.tx, + bucket: blockListBucket(username), + iterFn: func(_, b []byte) error { + var item blocklistmodel.Item + if err := item.UnmarshalBinary(b); err != nil { + return err + } + retVal = append(retVal, &item) + return nil + }, + } + if err := op.do(); err != nil { + return nil, err + } + return retVal, nil +} + +func (r *boltDBBlockListRep) DeleteBlockListItems(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: blockListBucket(username), + } + return op.do() +} + +func blockListBucket(username string) string { + return fmt.Sprintf("blocklist:%s", username) +} + +// UpsertBlockListItem satisfies repository.BlockList interface. +func (r *Repository) UpsertBlockListItem(ctx context.Context, item *blocklistmodel.Item) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newBlockListRep(tx).UpsertBlockListItem(ctx, item) + }) +} + +// DeleteBlockListItem deletes a block list item entity from storage. +func (r *Repository) DeleteBlockListItem(ctx context.Context, item *blocklistmodel.Item) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newBlockListRep(tx).DeleteBlockListItem(ctx, item) + }) +} + +// FetchBlockListItems retrieves from storage all block list items associated to a user. +func (r *Repository) FetchBlockListItems(ctx context.Context, username string) (items []*blocklistmodel.Item, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + items, err = newBlockListRep(tx).FetchBlockListItems(ctx, username) + return err + }) + return +} + +// DeleteBlockListItems deletes all block list items associated to a user. +func (r *Repository) DeleteBlockListItems(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newBlockListRep(tx).DeleteBlockListItems(ctx, username) + }) +} diff --git a/pkg/storage/boltdb/blocklist_test.go b/pkg/storage/boltdb/blocklist_test.go new file mode 100644 index 000000000..025068fdc --- /dev/null +++ b/pkg/storage/boltdb/blocklist_test.go @@ -0,0 +1,130 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + blocklistmodel "github.com/ortuman/jackal/pkg/model/blocklist" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_UpsertAndFetchBlockListItems(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBBlockListRep{tx: tx} + + err := rep.UpsertBlockListItem(context.Background(), &blocklistmodel.Item{ + Username: "ortuman", + Jid: "foo-1@jackal.im", + }) + require.NoError(t, err) + + err = rep.UpsertBlockListItem(context.Background(), &blocklistmodel.Item{ + Username: "ortuman", + Jid: "foo-2@jackal.im", + }) + require.NoError(t, err) + + items, err := rep.FetchBlockListItems(context.Background(), "ortuman") + require.NoError(t, err) + + require.Len(t, items, 2) + + require.Equal(t, "foo-1@jackal.im", items[0].Jid) + require.Equal(t, "foo-2@jackal.im", items[1].Jid) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeleteBlockListItem(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBBlockListRep{tx: tx} + + err := rep.UpsertBlockListItem(context.Background(), &blocklistmodel.Item{ + Username: "ortuman", + Jid: "foo-1@jackal.im", + }) + require.NoError(t, err) + + items, err := rep.FetchBlockListItems(context.Background(), "ortuman") + require.NoError(t, err) + + require.Len(t, items, 1) + + err = rep.DeleteBlockListItem(context.Background(), &blocklistmodel.Item{ + Username: "ortuman", + Jid: "foo-1@jackal.im", + }) + require.NoError(t, err) + + items, err = rep.FetchBlockListItems(context.Background(), "ortuman") + require.NoError(t, err) + + require.Len(t, items, 0) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeleteBlockListItems(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBBlockListRep{tx: tx} + + err := rep.UpsertBlockListItem(context.Background(), &blocklistmodel.Item{ + Username: "ortuman", + Jid: "foo-1@jackal.im", + }) + require.NoError(t, err) + + err = rep.UpsertBlockListItem(context.Background(), &blocklistmodel.Item{ + Username: "ortuman", + Jid: "foo-2@jackal.im", + }) + require.NoError(t, err) + + items, err := rep.FetchBlockListItems(context.Background(), "ortuman") + require.NoError(t, err) + + require.Len(t, items, 2) + + err = rep.DeleteBlockListItems(context.Background(), "ortuman") + require.NoError(t, err) + + items, err = rep.FetchBlockListItems(context.Background(), "ortuman") + require.NoError(t, err) + + require.Len(t, items, 0) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/capabilities.go b/pkg/storage/boltdb/capabilities.go new file mode 100644 index 000000000..f26c2dd98 --- /dev/null +++ b/pkg/storage/boltdb/capabilities.go @@ -0,0 +1,99 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + capsmodel "github.com/ortuman/jackal/pkg/model/caps" + bolt "go.etcd.io/bbolt" +) + +const capsKey = "caps" + +type boltDBCapsRep struct { + tx *bolt.Tx +} + +func newCapsRep(tx *bolt.Tx) *boltDBCapsRep { + return &boltDBCapsRep{tx: tx} +} + +func (r *boltDBCapsRep) UpsertCapabilities(_ context.Context, caps *capsmodel.Capabilities) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: capsBucketKey(caps.Node, caps.Ver), + key: capsKey, + obj: caps, + } + return op.do() +} + +func (r *boltDBCapsRep) CapabilitiesExist(_ context.Context, node, ver string) (bool, error) { + op := bucketExistsOp{ + tx: r.tx, + bucket: capsBucketKey(node, ver), + } + return op.do(), nil +} + +func (r *boltDBCapsRep) FetchCapabilities(_ context.Context, node, ver string) (*capsmodel.Capabilities, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: capsBucketKey(node, ver), + key: capsKey, + obj: &capsmodel.Capabilities{}, + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(*capsmodel.Capabilities), nil + default: + return nil, nil + } +} + +func capsBucketKey(node, ver string) string { + return fmt.Sprintf("caps:%s:%s", node, ver) +} + +// UpsertCapabilities satisfies repository.Capabilities interface. +func (r *Repository) UpsertCapabilities(ctx context.Context, caps *capsmodel.Capabilities) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newCapsRep(tx).UpsertCapabilities(ctx, caps) + }) +} + +// CapabilitiesExist tells whether node+ver capabilities have been already registered. +func (r *Repository) CapabilitiesExist(ctx context.Context, node, ver string) (ok bool, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + ok, err = newCapsRep(tx).CapabilitiesExist(ctx, node, ver) + return err + }) + return +} + +// FetchCapabilities fetches capabilities associated to a given node+ver pair. +func (r *Repository) FetchCapabilities(ctx context.Context, node, ver string) (caps *capsmodel.Capabilities, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + caps, err = newCapsRep(tx).FetchCapabilities(ctx, node, ver) + return err + }) + return +} diff --git a/pkg/storage/boltdb/capabilities_test.go b/pkg/storage/boltdb/capabilities_test.go new file mode 100644 index 000000000..9dfc3905c --- /dev/null +++ b/pkg/storage/boltdb/capabilities_test.go @@ -0,0 +1,73 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + capsmodel "github.com/ortuman/jackal/pkg/model/caps" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_UpsertAndFetchCapabilities(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBCapsRep{tx: tx} + + err := rep.UpsertCapabilities(context.Background(), &capsmodel.Capabilities{ + Node: "n1", + Ver: "v1", + }) + require.NoError(t, err) + + caps, err := rep.FetchCapabilities(context.Background(), "n1", "v1") + require.NoError(t, err) + + require.Equal(t, "n1", caps.Node) + require.Equal(t, "v1", caps.Ver) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_CapabilitiesExists(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBCapsRep{tx: tx} + + err := rep.UpsertCapabilities(context.Background(), &capsmodel.Capabilities{ + Node: "n1", + Ver: "v1", + }) + require.NoError(t, err) + + ok, err := rep.CapabilitiesExist(context.Background(), "n1", "v1") + require.NoError(t, err) + + require.True(t, ok) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/last.go b/pkg/storage/boltdb/last.go new file mode 100644 index 000000000..5e64f03a5 --- /dev/null +++ b/pkg/storage/boltdb/last.go @@ -0,0 +1,97 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + lastmodel "github.com/ortuman/jackal/pkg/model/last" + bolt "go.etcd.io/bbolt" +) + +const lastKey = "lst" + +type boltDBLastRep struct { + tx *bolt.Tx +} + +func newLastRep(tx *bolt.Tx) *boltDBLastRep { + return &boltDBLastRep{tx: tx} +} + +func (r *boltDBLastRep) UpsertLast(_ context.Context, last *lastmodel.Last) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: lastBucketKey(last.Username), + key: lastKey, + obj: last, + } + return op.do() +} + +func (r *boltDBLastRep) FetchLast(_ context.Context, username string) (*lastmodel.Last, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: lastBucketKey(username), + key: lastKey, + obj: &lastmodel.Last{}, + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(*lastmodel.Last), nil + default: + return nil, nil + } +} + +func (r *boltDBLastRep) DeleteLast(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: lastBucketKey(username), + } + return op.do() +} + +func lastBucketKey(username string) string { + return fmt.Sprintf("last:%s", username) +} + +// UpsertLast satisfies repository.Last interface. +func (r *Repository) UpsertLast(ctx context.Context, last *lastmodel.Last) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newLastRep(tx).UpsertLast(ctx, last) + }) +} + +// FetchLast satisfies repository.Last interface. +func (r *Repository) FetchLast(ctx context.Context, username string) (lst *lastmodel.Last, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + lst, err = newLastRep(tx).FetchLast(ctx, username) + return err + }) + return +} + +// DeleteLast satisfies repository.Last interface. +func (r *Repository) DeleteLast(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newLastRep(tx).DeleteLast(ctx, username) + }) +} diff --git a/pkg/storage/boltdb/last_test.go b/pkg/storage/boltdb/last_test.go new file mode 100644 index 000000000..c53473d5e --- /dev/null +++ b/pkg/storage/boltdb/last_test.go @@ -0,0 +1,77 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + lastmodel "github.com/ortuman/jackal/pkg/model/last" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_UpsertAndFetchLast(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBLastRep{tx: tx} + + err := rep.UpsertLast(context.Background(), &lastmodel.Last{ + Username: "ortuman", + Seconds: 10, + Status: "gone", + }) + require.NoError(t, err) + + lst, err := rep.FetchLast(context.Background(), "ortuman") + require.NoError(t, err) + + require.Equal(t, "ortuman", lst.Username) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeleteLast(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBLastRep{tx: tx} + + err := rep.UpsertLast(context.Background(), &lastmodel.Last{ + Username: "ortuman", + Seconds: 10, + Status: "gone", + }) + require.NoError(t, err) + + err = rep.DeleteLast(context.Background(), "ortuman") + require.NoError(t, err) + + lst, err := rep.FetchLast(context.Background(), "ortuman") + require.NoError(t, err) + + require.Nil(t, lst) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/locker.go b/pkg/storage/boltdb/locker.go new file mode 100644 index 000000000..97d87106a --- /dev/null +++ b/pkg/storage/boltdb/locker.go @@ -0,0 +1,34 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" +) + +type boltDBLocker struct{} + +func newLockerRep() *boltDBLocker { + return &boltDBLocker{} +} + +func (l *boltDBLocker) Lock(_ context.Context, _ string) error { return nil } +func (l *boltDBLocker) Unlock(_ context.Context, _ string) error { return nil } + +// Lock satisfies repository.Locker interface. +func (r *Repository) Lock(_ context.Context, _ string) error { return nil } + +// Unlock satisfies repository.Locker interface. +func (r *Repository) Unlock(_ context.Context, _ string) error { return nil } diff --git a/pkg/storage/boltdb/offline.go b/pkg/storage/boltdb/offline.go new file mode 100644 index 000000000..499b6ccbd --- /dev/null +++ b/pkg/storage/boltdb/offline.go @@ -0,0 +1,118 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + "github.com/golang/protobuf/proto" + "github.com/jackal-xmpp/stravaganza" + bolt "go.etcd.io/bbolt" +) + +type boltDBOfflineRep struct { + tx *bolt.Tx +} + +func newOfflineRep(tx *bolt.Tx) *boltDBOfflineRep { + return &boltDBOfflineRep{tx: tx} +} + +func (r *boltDBOfflineRep) InsertOfflineMessage(_ context.Context, message *stravaganza.Message, username string) error { + op := insertSeqOp{ + tx: r.tx, + bucket: offlineBucket(username), + obj: message, + } + return op.do() +} + +func (r *boltDBOfflineRep) CountOfflineMessages(_ context.Context, username string) (int, error) { + op := countKeysOp{ + tx: r.tx, + bucket: offlineBucket(username), + } + return op.do() +} + +func (r *boltDBOfflineRep) FetchOfflineMessages(_ context.Context, username string) ([]*stravaganza.Message, error) { + var retVal []*stravaganza.Message + + op := iterKeysOp{ + tx: r.tx, + bucket: offlineBucket(username), + iterFn: func(_, b []byte) error { + var elem stravaganza.PBElement + if err := proto.Unmarshal(b, &elem); err != nil { + return err + } + msg, err := stravaganza.NewBuilderFromProto(&elem).BuildMessage() + if err != nil { + return err + } + retVal = append(retVal, msg) + return nil + }, + } + if err := op.do(); err != nil { + return nil, err + } + return retVal, nil +} + +func (r *boltDBOfflineRep) DeleteOfflineMessages(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: offlineBucket(username), + } + return op.do() +} + +func offlineBucket(username string) string { + return fmt.Sprintf("offline:%s", username) +} + +// InsertOfflineMessage satisfies repository.Offline interface. +func (r *Repository) InsertOfflineMessage(ctx context.Context, message *stravaganza.Message, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newOfflineRep(tx).InsertOfflineMessage(ctx, message, username) + }) +} + +// CountOfflineMessages satisfies repository.Offline interface. +func (r *Repository) CountOfflineMessages(ctx context.Context, username string) (c int, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + c, err = newOfflineRep(tx).CountOfflineMessages(ctx, username) + return err + }) + return +} + +// FetchOfflineMessages satisfies repository.Offline interface. +func (r *Repository) FetchOfflineMessages(ctx context.Context, username string) (msg []*stravaganza.Message, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + msg, err = newOfflineRep(tx).FetchOfflineMessages(ctx, username) + return err + }) + return +} + +// DeleteOfflineMessages satisfies repository.Offline interface. +func (r *Repository) DeleteOfflineMessages(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newOfflineRep(tx).DeleteOfflineMessages(ctx, username) + }) +} diff --git a/pkg/storage/boltdb/offline_test.go b/pkg/storage/boltdb/offline_test.go new file mode 100644 index 000000000..cc9955ad3 --- /dev/null +++ b/pkg/storage/boltdb/offline_test.go @@ -0,0 +1,113 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_InsertAndFetchOfflineMessages(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBOfflineRep{tx: tx} + + m0 := testMessageStanza("message 0") + m1 := testMessageStanza("message 1") + + err := rep.InsertOfflineMessage(context.Background(), m0, "ortuman") + require.NoError(t, err) + + err = rep.InsertOfflineMessage(context.Background(), m1, "ortuman") + require.NoError(t, err) + + messages, err := rep.FetchOfflineMessages(context.Background(), "ortuman") + require.NoError(t, err) + + require.Len(t, messages, 2) + + require.Equal(t, "message 0", messages[0].Child("body").Text()) + require.Equal(t, "message 1", messages[1].Child("body").Text()) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_CountOfflineMessages(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBOfflineRep{tx: tx} + + m0 := testMessageStanza("message 0") + m1 := testMessageStanza("message 1") + + err := rep.InsertOfflineMessage(context.Background(), m0, "ortuman") + require.NoError(t, err) + + err = rep.InsertOfflineMessage(context.Background(), m1, "ortuman") + require.NoError(t, err) + + cnt, err := rep.CountOfflineMessages(context.Background(), "ortuman") + require.NoError(t, err) + + require.Equal(t, 2, cnt) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeleteOfflineMessages(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBOfflineRep{tx: tx} + + m0 := testMessageStanza("message 0") + m1 := testMessageStanza("message 1") + + err := rep.InsertOfflineMessage(context.Background(), m0, "ortuman") + require.NoError(t, err) + + err = rep.InsertOfflineMessage(context.Background(), m1, "ortuman") + require.NoError(t, err) + + cnt, err := rep.CountOfflineMessages(context.Background(), "ortuman") + require.NoError(t, err) + require.Equal(t, 2, cnt) + + err = rep.DeleteOfflineMessages(context.Background(), "ortuman") + require.NoError(t, err) + + cnt, err = rep.CountOfflineMessages(context.Background(), "ortuman") + require.NoError(t, err) + require.Equal(t, 0, cnt) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/op.go b/pkg/storage/boltdb/op.go new file mode 100644 index 000000000..f92bd3257 --- /dev/null +++ b/pkg/storage/boltdb/op.go @@ -0,0 +1,159 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "fmt" + + bolt "go.etcd.io/bbolt" + + "github.com/ortuman/jackal/pkg/model" +) + +type upsertKeyOp struct { + tx *bolt.Tx + bucket string + key string + obj model.Codec +} + +func (op upsertKeyOp) do() error { + b, err := op.tx.CreateBucketIfNotExists([]byte(op.bucket)) + if err != nil { + return err + } + p, err := op.obj.MarshalBinary() + if err != nil { + return err + } + return b.Put([]byte(op.key), p) +} + +type insertSeqOp struct { + tx *bolt.Tx + bucket string + obj model.Codec +} + +func (op insertSeqOp) do() error { + b, err := op.tx.CreateBucketIfNotExists([]byte(op.bucket)) + if err != nil { + return err + } + p, err := op.obj.MarshalBinary() + if err != nil { + return err + } + seq, err := b.NextSequence() + if err != nil { + return err + } + k := fmt.Sprintf("%d", seq) + return b.Put([]byte(k), p) +} + +type delBucketOp struct { + tx *bolt.Tx + bucket string +} + +func (op delBucketOp) do() error { + return op.tx.DeleteBucket([]byte(op.bucket)) +} + +type delKeyOp struct { + tx *bolt.Tx + bucket string + key string +} + +func (op delKeyOp) do() error { + b := op.tx.Bucket([]byte(op.bucket)) + if b == nil { + return nil + } + return b.Delete([]byte(op.key)) +} + +type bucketExistsOp struct { + tx *bolt.Tx + bucket string +} + +func (op bucketExistsOp) do() bool { + return op.tx.Bucket([]byte(op.bucket)) != nil +} + +type fetchKeyOp struct { + tx *bolt.Tx + bucket string + key string + obj model.Codec +} + +func (op fetchKeyOp) do() (model.Codec, error) { + b := op.tx.Bucket([]byte(op.bucket)) + if b == nil { + return nil, nil + } + data := b.Get([]byte(op.key)) + if data == nil { + return nil, nil + } + if err := op.obj.UnmarshalBinary(data); err != nil { + return nil, err + } + return op.obj, nil +} + +type countKeysOp struct { + tx *bolt.Tx + bucket string +} + +func (op countKeysOp) do() (int, error) { + b := op.tx.Bucket([]byte(op.bucket)) + if b == nil { + return 0, nil + } + var retVal int + + c := b.Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + retVal++ + } + return retVal, nil +} + +type iterKeysOp struct { + tx *bolt.Tx + bucket string + iterFn func(k, b []byte) error +} + +func (op iterKeysOp) do() error { + b := op.tx.Bucket([]byte(op.bucket)) + if b == nil { + return nil + } + c := b.Cursor() + + for k, v := c.First(); k != nil; k, v = c.Next() { + if err := op.iterFn(k, v); err != nil { + return err + } + } + return nil +} diff --git a/pkg/storage/boltdb/private.go b/pkg/storage/boltdb/private.go new file mode 100644 index 000000000..cc37d676e --- /dev/null +++ b/pkg/storage/boltdb/private.go @@ -0,0 +1,95 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + "github.com/jackal-xmpp/stravaganza" + bolt "go.etcd.io/bbolt" +) + +type boltDBPrivateRep struct { + tx *bolt.Tx +} + +func newPrivateRep(tx *bolt.Tx) *boltDBPrivateRep { + return &boltDBPrivateRep{tx: tx} +} + +func (r *boltDBPrivateRep) FetchPrivate(_ context.Context, namespace, username string) (stravaganza.Element, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: privateBucketKey(username), + key: namespace, + obj: stravaganza.EmptyElement(), + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(stravaganza.Element), nil + default: + return nil, nil + } +} + +func (r *boltDBPrivateRep) UpsertPrivate(_ context.Context, private stravaganza.Element, namespace, username string) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: privateBucketKey(username), + key: namespace, + obj: private, + } + return op.do() +} + +func (r *boltDBPrivateRep) DeletePrivates(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: privateBucketKey(username), + } + return op.do() +} + +func privateBucketKey(username string) string { + return fmt.Sprintf("prv:%s", username) +} + +// FetchPrivate satisfies repository.Private interface. +func (r *Repository) FetchPrivate(ctx context.Context, namespace, username string) (prv stravaganza.Element, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + prv, err = newPrivateRep(tx).FetchPrivate(ctx, namespace, username) + return err + }) + return +} + +// UpsertPrivate satisfies repository.Private interface. +func (r *Repository) UpsertPrivate(ctx context.Context, private stravaganza.Element, namespace, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newPrivateRep(tx).UpsertPrivate(ctx, private, namespace, username) + }) +} + +// DeletePrivates satisfies repository.Private interface. +func (r *Repository) DeletePrivates(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newPrivateRep(tx).DeletePrivates(ctx, username) + }) +} diff --git a/pkg/storage/boltdb/private_test.go b/pkg/storage/boltdb/private_test.go new file mode 100644 index 000000000..aa00b8980 --- /dev/null +++ b/pkg/storage/boltdb/private_test.go @@ -0,0 +1,73 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + "github.com/jackal-xmpp/stravaganza" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_UpsertAndFetchPrivate(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBPrivateRep{tx: tx} + + prv0 := stravaganza.NewBuilder("prv").Build() + + err := rep.UpsertPrivate(context.Background(), prv0, "ns1", "ortuman") + require.NoError(t, err) + + prv, err := rep.FetchPrivate(context.Background(), "ns1", "ortuman") + require.NoError(t, err) + + require.Equal(t, "prv", prv.Name()) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeletePrivate(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBPrivateRep{tx: tx} + + prv0 := stravaganza.NewBuilder("prv").Build() + + err := rep.UpsertPrivate(context.Background(), prv0, "ns1", "ortuman") + require.NoError(t, err) + + err = rep.DeletePrivates(context.Background(), "ortuman") + require.NoError(t, err) + + prv, err := rep.FetchPrivate(context.Background(), "ns1", "ortuman") + require.NoError(t, err) + + require.Nil(t, prv) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/repository.go b/pkg/storage/boltdb/repository.go new file mode 100644 index 000000000..aaa9faa58 --- /dev/null +++ b/pkg/storage/boltdb/repository.go @@ -0,0 +1,93 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "time" + + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/ortuman/jackal/pkg/storage/repository" + bolt "go.etcd.io/bbolt" +) + +// Config contains BoltDB configuration value. +type Config struct { + Path string `fig:"path" default:".jackal.db"` +} + +// Repository represents a BoltDB repository implementation. +type Repository struct { + repository.User + repository.Last + repository.Capabilities + repository.Offline + repository.BlockList + repository.Private + repository.Roster + repository.VCard + repository.Locker + + cfg Config + + db *bolt.DB + logger kitlog.Logger +} + +// New creates and returns an initialized BoltDB Repository instance. +func New(cfg Config, logger kitlog.Logger) *Repository { + return &Repository{ + cfg: cfg, + logger: logger, + } +} + +// InTransaction generates a BoltDB transaction and completes it after it's being used by f function. +func (r *Repository) InTransaction(ctx context.Context, f func(ctx context.Context, tx repository.Transaction) error) error { + tx, err := r.db.Begin(true) + if err != nil { + return err + } + repTx := newRepTx(tx) + if err := f(ctx, repTx); err != nil { + if err := tx.Rollback(); err != nil { + level.Warn(r.logger).Log("msg", "failed to rollback BoltDB transaction", "err", err) + } + return err + } + return tx.Commit() +} + +// Start implements Start interface method. +func (r *Repository) Start(_ context.Context) error { + db, err := bolt.Open(r.cfg.Path, 0600, &bolt.Options{Timeout: time.Second}) + if err != nil { + return err + } + r.db = db + + level.Info(r.logger).Log("msg", "started BoltDB repository") + return nil +} + +// Stop closes BoltDB database. +func (r *Repository) Stop(_ context.Context) error { + if err := r.db.Close(); err != nil { + return err + } + level.Info(r.logger).Log("msg", "stopped BoltDB repository") + return nil +} diff --git a/pkg/storage/boltdb/roster.go b/pkg/storage/boltdb/roster.go new file mode 100644 index 000000000..73c1362a6 --- /dev/null +++ b/pkg/storage/boltdb/roster.go @@ -0,0 +1,409 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + "sort" + + "github.com/golang/protobuf/proto" + rostermodel "github.com/ortuman/jackal/pkg/model/roster" + bolt "go.etcd.io/bbolt" +) + +const versionKey = "ver" + +type boltDBRosterRep struct { + tx *bolt.Tx +} + +func newRosterRep(tx *bolt.Tx) *boltDBRosterRep { + return &boltDBRosterRep{tx: tx} +} + +func (r *boltDBRosterRep) TouchRosterVersion(_ context.Context, username string) (int, error) { + var ver *rostermodel.Version + + fetchOp := fetchKeyOp{ + tx: r.tx, + bucket: rosterVersionBucketKey(username), + key: versionKey, + obj: &rostermodel.Version{}, + } + obj, err := fetchOp.do() + if err != nil { + return 0, err + } + switch { + case obj != nil: + ver = obj.(*rostermodel.Version) + ver.Version++ + default: + ver = &rostermodel.Version{Version: 1} + } + + upsertOp := upsertKeyOp{ + tx: r.tx, + bucket: rosterVersionBucketKey(username), + key: versionKey, + obj: ver, + } + if err := upsertOp.do(); err != nil { + return 0, err + } + return int(ver.Version), nil +} + +func (r *boltDBRosterRep) FetchRosterVersion(_ context.Context, username string) (int, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: rosterVersionBucketKey(username), + key: versionKey, + obj: &rostermodel.Version{}, + } + obj, err := op.do() + if err != nil { + return 0, err + } + switch { + case obj != nil: + return int(obj.(*rostermodel.Version).Version), nil + default: + return 0, nil + } +} + +func (r *boltDBRosterRep) UpsertRosterItem(_ context.Context, ri *rostermodel.Item) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(ri.Username), + key: ri.Jid, + obj: ri, + } + return op.do() +} + +func (r *boltDBRosterRep) DeleteRosterItem(_ context.Context, username, jid string) error { + op := delKeyOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(username), + key: jid, + } + return op.do() +} + +func (r *boltDBRosterRep) DeleteRosterItems(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(username), + } + return op.do() +} + +func (r *boltDBRosterRep) FetchRosterItems(_ context.Context, username string) ([]*rostermodel.Item, error) { + var retVal []*rostermodel.Item + + op := iterKeysOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(username), + iterFn: func(_, b []byte) error { + var itm rostermodel.Item + if err := proto.Unmarshal(b, &itm); err != nil { + return err + } + retVal = append(retVal, &itm) + return nil + }, + } + if err := op.do(); err != nil { + return nil, err + } + return retVal, nil +} + +func (r *boltDBRosterRep) FetchRosterItemsInGroups(_ context.Context, username string, groups []string) ([]*rostermodel.Item, error) { + var retVal []*rostermodel.Item + + groupsMap := make(map[string]struct{}, len(groups)) + for _, gr := range groups { + groupsMap[gr] = struct{}{} + } + op := iterKeysOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(username), + iterFn: func(_, b []byte) error { + var itm rostermodel.Item + if err := proto.Unmarshal(b, &itm); err != nil { + return err + } + for _, gr := range itm.Groups { + _, ok := groupsMap[gr] + if ok { + // item in group + retVal = append(retVal, &itm) + return nil + } + } + return nil + }, + } + if err := op.do(); err != nil { + return nil, err + } + return retVal, nil +} + +func (r *boltDBRosterRep) FetchRosterItem(_ context.Context, username, jid string) (*rostermodel.Item, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(username), + key: jid, + obj: &rostermodel.Item{}, + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(*rostermodel.Item), nil + default: + return nil, nil + } +} + +func (r *boltDBRosterRep) FetchRosterGroups(_ context.Context, username string) ([]string, error) { + groupsMap := make(map[string]struct{}) + + op := iterKeysOp{ + tx: r.tx, + bucket: rosterItemsBucketKey(username), + iterFn: func(_, b []byte) error { + var itm rostermodel.Item + if err := proto.Unmarshal(b, &itm); err != nil { + return err + } + for _, gr := range itm.Groups { + groupsMap[gr] = struct{}{} + } + return nil + }, + } + if err := op.do(); err != nil { + return nil, err + } + var retVal []string + + for gr := range groupsMap { + retVal = append(retVal, gr) + } + sort.Slice(retVal, func(i, j int) bool { return retVal[i] < retVal[j] }) + + return retVal, nil +} + +func (r *boltDBRosterRep) UpsertRosterNotification(_ context.Context, rn *rostermodel.Notification) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: rosterNotificationsBucketKey(rn.Contact), + key: rn.Jid, + obj: rn, + } + return op.do() +} + +func (r *boltDBRosterRep) DeleteRosterNotification(_ context.Context, contact, jid string) error { + op := delKeyOp{ + tx: r.tx, + bucket: rosterNotificationsBucketKey(contact), + key: jid, + } + return op.do() +} + +func (r *boltDBRosterRep) DeleteRosterNotifications(_ context.Context, contact string) error { + op := delBucketOp{ + tx: r.tx, + bucket: rosterNotificationsBucketKey(contact), + } + return op.do() +} + +func (r *boltDBRosterRep) FetchRosterNotification(_ context.Context, contact string, jid string) (*rostermodel.Notification, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: rosterNotificationsBucketKey(contact), + key: jid, + obj: &rostermodel.Notification{}, + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(*rostermodel.Notification), nil + default: + return nil, nil + } +} + +func (r *boltDBRosterRep) FetchRosterNotifications(_ context.Context, contact string) ([]*rostermodel.Notification, error) { + var retVal []*rostermodel.Notification + + op := iterKeysOp{ + tx: r.tx, + bucket: rosterNotificationsBucketKey(contact), + iterFn: func(_, b []byte) error { + var not rostermodel.Notification + if err := proto.Unmarshal(b, ¬); err != nil { + return err + } + retVal = append(retVal, ¬) + return nil + }, + } + if err := op.do(); err != nil { + return nil, err + } + return retVal, nil +} + +func rosterVersionBucketKey(username string) string { + return fmt.Sprintf("roster:ver:%s", username) +} + +func rosterItemsBucketKey(username string) string { + return fmt.Sprintf("roster:items:%s", username) +} + +func rosterNotificationsBucketKey(username string) string { + return fmt.Sprintf("roster:notif:%s", username) +} + +// TouchRosterVersion satisfies repository.Roster interface. +func (r *Repository) TouchRosterVersion(ctx context.Context, username string) (v int, err error) { + err = r.db.Update(func(tx *bolt.Tx) error { + v, err = newRosterRep(tx).TouchRosterVersion(ctx, username) + return err + }) + return +} + +// FetchRosterVersion satisfies repository.Roster interface. +func (r *Repository) FetchRosterVersion(ctx context.Context, username string) (v int, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + v, err = newRosterRep(tx).FetchRosterVersion(ctx, username) + return err + }) + return +} + +// UpsertRosterItem satisfies repository.Roster interface. +func (r *Repository) UpsertRosterItem(ctx context.Context, ri *rostermodel.Item) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newRosterRep(tx).UpsertRosterItem(ctx, ri) + }) +} + +// DeleteRosterItem satisfies repository.Roster interface. +func (r *Repository) DeleteRosterItem(ctx context.Context, username, jid string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newRosterRep(tx).DeleteRosterItem(ctx, username, jid) + }) +} + +// DeleteRosterItems satisfies repository.Roster interface. +func (r *Repository) DeleteRosterItems(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newRosterRep(tx).DeleteRosterItems(ctx, username) + }) +} + +// FetchRosterItems satisfies repository.Roster interface. +func (r *Repository) FetchRosterItems(ctx context.Context, username string) (items []*rostermodel.Item, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + items, err = newRosterRep(tx).FetchRosterItems(ctx, username) + return err + }) + return +} + +// FetchRosterItemsInGroups satisfies repository.Roster interface. +func (r *Repository) FetchRosterItemsInGroups(ctx context.Context, username string, groups []string) (items []*rostermodel.Item, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + items, err = newRosterRep(tx).FetchRosterItemsInGroups(ctx, username, groups) + return err + }) + return +} + +// FetchRosterItem satisfies repository.Roster interface. +func (r *Repository) FetchRosterItem(ctx context.Context, username, jid string) (item *rostermodel.Item, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + item, err = newRosterRep(tx).FetchRosterItem(ctx, username, jid) + return err + }) + return +} + +// UpsertRosterNotification satisfies repository.Roster interface. +func (r *Repository) UpsertRosterNotification(ctx context.Context, rn *rostermodel.Notification) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newRosterRep(tx).UpsertRosterNotification(ctx, rn) + }) +} + +// DeleteRosterNotification satisfies repository.Roster interface. +func (r *Repository) DeleteRosterNotification(ctx context.Context, contact, jid string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newRosterRep(tx).DeleteRosterNotification(ctx, contact, jid) + }) +} + +// DeleteRosterNotifications satisfies repository.Roster interface. +func (r *Repository) DeleteRosterNotifications(ctx context.Context, contact string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newRosterRep(tx).DeleteRosterNotifications(ctx, contact) + }) +} + +// FetchRosterNotification satisfies repository.Roster interface. +func (r *Repository) FetchRosterNotification(ctx context.Context, contact string, jid string) (n *rostermodel.Notification, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + n, err = newRosterRep(tx).FetchRosterNotification(ctx, contact, jid) + return err + }) + return +} + +// FetchRosterNotifications satisfies repository.Roster interface. +func (r *Repository) FetchRosterNotifications(ctx context.Context, contact string) (ns []*rostermodel.Notification, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + ns, err = newRosterRep(tx).FetchRosterNotifications(ctx, contact) + return err + }) + return +} + +// FetchRosterGroups satisfies repository.Roster interface. +func (r *Repository) FetchRosterGroups(ctx context.Context, username string) (groups []string, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + groups, err = newRosterRep(tx).FetchRosterGroups(ctx, username) + return err + }) + return +} diff --git a/pkg/storage/boltdb/roster_test.go b/pkg/storage/boltdb/roster_test.go new file mode 100644 index 000000000..e785d1ade --- /dev/null +++ b/pkg/storage/boltdb/roster_test.go @@ -0,0 +1,187 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + rostermodel "github.com/ortuman/jackal/pkg/model/roster" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_TouchAndFetchRosterVersion(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBRosterRep{tx: tx} + + ver, err := rep.TouchRosterVersion(context.Background(), "ortuman") + require.NoError(t, err) + require.Equal(t, 1, ver) + + ver, err = rep.FetchRosterVersion(context.Background(), "ortuman") + require.NoError(t, err) + require.Equal(t, 1, ver) + + ver, err = rep.TouchRosterVersion(context.Background(), "ortuman") + require.NoError(t, err) + require.Equal(t, 2, ver) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_RosterItems(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBRosterRep{tx: tx} + + err := rep.UpsertRosterItem(context.Background(), &rostermodel.Item{ + Username: "ortuman", + Jid: "foo@jackal.im", + Groups: []string{"g1"}, + }) + require.NoError(t, err) + + err = rep.UpsertRosterItem(context.Background(), &rostermodel.Item{ + Username: "ortuman", + Jid: "foo-2@jackal.im", + Groups: []string{"g2"}, + }) + require.NoError(t, err) + + itm, err := rep.FetchRosterItem(context.Background(), "ortuman", "foo@jackal.im") + require.NoError(t, err) + require.NotNil(t, itm) + require.Equal(t, "foo@jackal.im", itm.Jid) + + items, err := rep.FetchRosterItems(context.Background(), "ortuman") + require.NoError(t, err) + require.Len(t, items, 2) + + items, err = rep.FetchRosterItemsInGroups(context.Background(), "ortuman", []string{"g2"}) + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, "foo-2@jackal.im", items[0].Jid) + + err = rep.DeleteRosterItem(context.Background(), "ortuman", "foo-2@jackal.im") + require.NoError(t, err) + + items, err = rep.FetchRosterItems(context.Background(), "ortuman") + require.NoError(t, err) + require.Len(t, items, 1) + + err = rep.DeleteRosterItems(context.Background(), "ortuman") + require.NoError(t, err) + + items, err = rep.FetchRosterItems(context.Background(), "ortuman") + require.NoError(t, err) + require.Len(t, items, 0) + + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_RosterNotifications(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBRosterRep{tx: tx} + + err := rep.UpsertRosterNotification(context.Background(), &rostermodel.Notification{ + Contact: "ortuman", + Jid: "foo-1@jackal.im", + }) + require.NoError(t, err) + + err = rep.UpsertRosterNotification(context.Background(), &rostermodel.Notification{ + Contact: "ortuman", + Jid: "foo-2@jackal.im", + }) + require.NoError(t, err) + + n, err := rep.FetchRosterNotification(context.Background(), "ortuman", "foo-1@jackal.im") + require.NoError(t, err) + require.NotNil(t, n) + require.Equal(t, "foo-1@jackal.im", n.Jid) + + ns, err := rep.FetchRosterNotifications(context.Background(), "ortuman") + require.NoError(t, err) + require.Len(t, ns, 2) + + err = rep.DeleteRosterNotification(context.Background(), "ortuman", "foo-2@jackal.im") + require.NoError(t, err) + + ns, err = rep.FetchRosterNotifications(context.Background(), "ortuman") + require.NoError(t, err) + require.Len(t, ns, 1) + + err = rep.DeleteRosterNotifications(context.Background(), "ortuman") + require.NoError(t, err) + + ns, err = rep.FetchRosterNotifications(context.Background(), "ortuman") + require.NoError(t, err) + require.Len(t, ns, 0) + + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_TouchAndFetchRosterGroups(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBRosterRep{tx: tx} + + err := rep.UpsertRosterItem(context.Background(), &rostermodel.Item{ + Username: "ortuman", + Jid: "foo@jackal.im", + Groups: []string{"g1"}, + }) + require.NoError(t, err) + + err = rep.UpsertRosterItem(context.Background(), &rostermodel.Item{ + Username: "ortuman", + Jid: "foo-2@jackal.im", + Groups: []string{"g2"}, + }) + require.NoError(t, err) + + groups, err := rep.FetchRosterGroups(context.Background(), "ortuman") + require.NoError(t, err) + require.Contains(t, groups, "g1") + require.Contains(t, groups, "g2") + + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/tx.go b/pkg/storage/boltdb/tx.go new file mode 100644 index 000000000..2d7cbc29e --- /dev/null +++ b/pkg/storage/boltdb/tx.go @@ -0,0 +1,46 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "github.com/ortuman/jackal/pkg/storage/repository" + bolt "go.etcd.io/bbolt" +) + +type repTx struct { + repository.User + repository.Last + repository.Capabilities + repository.Offline + repository.BlockList + repository.Private + repository.Roster + repository.VCard + repository.Locker +} + +func newRepTx(tx *bolt.Tx) *repTx { + return &repTx{ + User: newUserRep(tx), + Last: newLastRep(tx), + Capabilities: newCapsRep(tx), + Offline: newOfflineRep(tx), + BlockList: newBlockListRep(tx), + Private: newPrivateRep(tx), + Roster: newRosterRep(tx), + VCard: newVCardRep(tx), + Locker: newLockerRep(), + } +} diff --git a/pkg/storage/boltdb/user.go b/pkg/storage/boltdb/user.go new file mode 100644 index 000000000..2c762e44c --- /dev/null +++ b/pkg/storage/boltdb/user.go @@ -0,0 +1,114 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + usermodel "github.com/ortuman/jackal/pkg/model/user" + bolt "go.etcd.io/bbolt" +) + +const userKey = "usr" + +type boltDBUserRep struct { + tx *bolt.Tx +} + +func newUserRep(tx *bolt.Tx) *boltDBUserRep { + return &boltDBUserRep{tx: tx} +} + +func (r *boltDBUserRep) UpsertUser(_ context.Context, user *usermodel.User) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: userBucketKey(user.Username), + key: userKey, + obj: user, + } + return op.do() +} + +func (r *boltDBUserRep) DeleteUser(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: userBucketKey(username), + } + return op.do() +} + +func (r *boltDBUserRep) FetchUser(_ context.Context, username string) (*usermodel.User, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: userBucketKey(username), + key: userKey, + obj: &usermodel.User{}, + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(*usermodel.User), nil + default: + return nil, nil + } +} + +func (r *boltDBUserRep) UserExists(_ context.Context, username string) (bool, error) { + op := bucketExistsOp{ + tx: r.tx, + bucket: userBucketKey(username), + } + return op.do(), nil +} + +func userBucketKey(username string) string { + return fmt.Sprintf("user:%s", username) +} + +// UpsertUser satisfies repository.User interface. +func (r *Repository) UpsertUser(ctx context.Context, user *usermodel.User) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newUserRep(tx).UpsertUser(ctx, user) + }) +} + +// DeleteUser satisfies repository.User interface. +func (r *Repository) DeleteUser(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newUserRep(tx).DeleteUser(ctx, username) + }) +} + +// FetchUser satisfies repository.User interface. +func (r *Repository) FetchUser(ctx context.Context, username string) (usr *usermodel.User, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + usr, err = newUserRep(tx).FetchUser(ctx, username) + return err + }) + return +} + +// UserExists satisfies repository.User interface. +func (r *Repository) UserExists(ctx context.Context, username string) (ok bool, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + ok, err = newUserRep(tx).UserExists(ctx, username) + return err + }) + return +} diff --git a/pkg/storage/boltdb/user_test.go b/pkg/storage/boltdb/user_test.go new file mode 100644 index 000000000..8a29faba9 --- /dev/null +++ b/pkg/storage/boltdb/user_test.go @@ -0,0 +1,96 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + usermodel "github.com/ortuman/jackal/pkg/model/user" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_UpsertAndFetchUser(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBUserRep{tx: tx} + + err := rep.UpsertUser(context.Background(), &usermodel.User{ + Username: "ortuman", + }) + require.NoError(t, err) + + usr, err := rep.FetchUser(context.Background(), "ortuman") + require.NoError(t, err) + + require.Equal(t, "ortuman", usr.Username) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_UserExists(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBUserRep{tx: tx} + + err := rep.UpsertUser(context.Background(), &usermodel.User{ + Username: "ortuman", + }) + require.NoError(t, err) + + ok, err := rep.UserExists(context.Background(), "ortuman") + require.NoError(t, err) + + require.True(t, ok) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeleteUser(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBUserRep{tx: tx} + + err := rep.UpsertUser(context.Background(), &usermodel.User{ + Username: "ortuman", + }) + require.NoError(t, err) + + err = rep.DeleteUser(context.Background(), "ortuman") + require.NoError(t, err) + + ok, err := rep.UserExists(context.Background(), "ortuman") + require.NoError(t, err) + + require.False(t, ok) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/boltdb/util_test.go b/pkg/storage/boltdb/util_test.go new file mode 100644 index 000000000..02a9ef18d --- /dev/null +++ b/pkg/storage/boltdb/util_test.go @@ -0,0 +1,55 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "fmt" + "os" + "testing" + + "github.com/jackal-xmpp/stravaganza" + + bolt "go.etcd.io/bbolt" +) + +func setupDB(t *testing.T) *bolt.DB { + t.Helper() + + dbPath := fmt.Sprintf("%s/test.db", t.TempDir()) + db, err := bolt.Open(dbPath, 0666, nil) + if err != nil { + t.Fatal(err) + } + return db +} + +func cleanUp(db *bolt.DB) { + dbPath := db.Path() + _ = db.Close() + _ = os.RemoveAll(dbPath) +} + +func testMessageStanza(body string) *stravaganza.Message { + b := stravaganza.NewMessageBuilder() + b.WithAttribute("from", "noelia@jackal.im/yard") + b.WithAttribute("to", "ortuman@jackal.im/balcony") + b.WithChild( + stravaganza.NewBuilder("body"). + WithText(body). + Build(), + ) + msg, _ := b.BuildMessage() + return msg +} diff --git a/pkg/storage/boltdb/vcard.go b/pkg/storage/boltdb/vcard.go new file mode 100644 index 000000000..37c37a476 --- /dev/null +++ b/pkg/storage/boltdb/vcard.go @@ -0,0 +1,97 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "fmt" + + "github.com/jackal-xmpp/stravaganza" + bolt "go.etcd.io/bbolt" +) + +const vCardKey = "vcard" + +type boltDBVCardRep struct { + tx *bolt.Tx +} + +func newVCardRep(tx *bolt.Tx) *boltDBVCardRep { + return &boltDBVCardRep{tx: tx} +} + +func (r *boltDBVCardRep) UpsertVCard(_ context.Context, vCard stravaganza.Element, username string) error { + op := upsertKeyOp{ + tx: r.tx, + bucket: vCardBucketKey(username), + key: vCardKey, + obj: vCard, + } + return op.do() +} + +func (r *boltDBVCardRep) FetchVCard(_ context.Context, username string) (stravaganza.Element, error) { + op := fetchKeyOp{ + tx: r.tx, + bucket: vCardBucketKey(username), + key: vCardKey, + obj: stravaganza.EmptyElement(), + } + obj, err := op.do() + if err != nil { + return nil, err + } + switch { + case obj != nil: + return obj.(stravaganza.Element), nil + default: + return nil, nil + } +} + +func (r *boltDBVCardRep) DeleteVCard(_ context.Context, username string) error { + op := delBucketOp{ + tx: r.tx, + bucket: vCardBucketKey(username), + } + return op.do() +} + +func vCardBucketKey(username string) string { + return fmt.Sprintf("vcard:%s", username) +} + +// UpsertVCard satisfies repository.VCard interface. +func (r *Repository) UpsertVCard(ctx context.Context, vCard stravaganza.Element, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newVCardRep(tx).UpsertVCard(ctx, vCard, username) + }) +} + +// FetchVCard satisfies repository.VCard interface. +func (r *Repository) FetchVCard(ctx context.Context, username string) (vc stravaganza.Element, err error) { + err = r.db.View(func(tx *bolt.Tx) error { + vc, err = newVCardRep(tx).FetchVCard(ctx, username) + return err + }) + return +} + +// DeleteVCard satisfies repository.VCard interface. +func (r *Repository) DeleteVCard(ctx context.Context, username string) error { + return r.db.Update(func(tx *bolt.Tx) error { + return newVCardRep(tx).DeleteVCard(ctx, username) + }) +} diff --git a/pkg/storage/boltdb/vcard_test.go b/pkg/storage/boltdb/vcard_test.go new file mode 100644 index 000000000..d76eb92e5 --- /dev/null +++ b/pkg/storage/boltdb/vcard_test.go @@ -0,0 +1,73 @@ +// Copyright 2022 The jackal Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "context" + "testing" + + "github.com/jackal-xmpp/stravaganza" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestBoltDB_UpsertAndFetchVCard(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBVCardRep{tx: tx} + + vc0 := stravaganza.NewBuilder("vc").Build() + + err := rep.UpsertVCard(context.Background(), vc0, "ortuman") + require.NoError(t, err) + + vc, err := rep.FetchVCard(context.Background(), "ortuman") + require.NoError(t, err) + + require.Equal(t, "vc", vc.Name()) + return nil + }) + require.NoError(t, err) +} + +func TestBoltDB_DeleteVCard(t *testing.T) { + t.Parallel() + + db := setupDB(t) + t.Cleanup(func() { cleanUp(db) }) + + err := db.Update(func(tx *bolt.Tx) error { + rep := boltDBVCardRep{tx: tx} + + vc := stravaganza.NewBuilder("vc").Build() + + err := rep.UpsertVCard(context.Background(), vc, "ortuman") + require.NoError(t, err) + + err = rep.DeleteVCard(context.Background(), "ortuman") + require.NoError(t, err) + + vc, err = rep.FetchVCard(context.Background(), "ortuman") + require.NoError(t, err) + + require.Nil(t, vc) + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index e07860001..a3989bc69 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -18,29 +18,41 @@ import ( "fmt" kitlog "github.com/go-kit/log" + "github.com/ortuman/jackal/pkg/storage/boltdb" cachedrepository "github.com/ortuman/jackal/pkg/storage/cached" measuredrepository "github.com/ortuman/jackal/pkg/storage/measured" pgsqlrepository "github.com/ortuman/jackal/pkg/storage/pgsql" "github.com/ortuman/jackal/pkg/storage/repository" ) -const pgSQLRepositoryType = "pgsql" +const ( + boltDBRepositoryType = "boltdb" + pgSQLRepositoryType = "pgsql" +) // Config contains generic storage configuration. type Config struct { - Type string `fig:"type" default:"pgsql"` - PgSQL pgsqlrepository.Config `fig:"pgsql"` - Cache cachedrepository.Config `fig:"cache"` + Type string `fig:"type" default:"boltdb"` + PgSQL pgsqlrepository.Config `fig:"pgsql"` + BoltDB boltdb.Config `fig:"boltdb"` + Cache cachedrepository.Config `fig:"cache"` } // New returns an initialized repository.Repository derived from cfg configuration. func New(cfg Config, logger kitlog.Logger) (repository.Repository, error) { - if cfg.Type != pgSQLRepositoryType { + var rep repository.Repository + + switch cfg.Type { + case pgSQLRepositoryType: + rep = pgsqlrepository.New(cfg.PgSQL, logger) + + case boltDBRepositoryType: + rep = boltdb.New(cfg.BoltDB, logger) + + default: return nil, fmt.Errorf("unrecognized repository type: %s", cfg.Type) } - var rep repository.Repository - rep = pgsqlrepository.New(cfg.PgSQL, logger) if len(cfg.Cache.Type) > 0 { var err error rep, err = cachedrepository.New(cfg.Cache, rep, logger)