Removed LedisDB persistence layer. May reimplement in the future (not likely thou)

This commit is contained in:
Deluan 2020-01-13 16:37:24 -05:00
parent 614f4afe28
commit 536244bc44
20 changed files with 14 additions and 1329 deletions

View File

@ -24,9 +24,8 @@ type sonic struct {
PlsIgnoredPatterns string `default:"^iCloud;\\~"`
// DevFlags
DevDisableAuthentication bool `default:"false"`
DevDisableFileCheck bool `default:"false"`
DevPersistenceProvider string `default:"ledisdb"`
DevDisableAuthentication bool `default:"false"`
DevDisableFileCheck bool `default:"false"`
}
var Sonic *sonic

1
go.mod
View File

@ -23,7 +23,6 @@ require (
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
github.com/onsi/ginkgo v1.11.0
github.com/onsi/gomega v1.8.1
github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373
github.com/sirupsen/logrus v1.4.2
github.com/smartystreets/assertions v1.0.1 // indirect
github.com/smartystreets/goconvey v1.6.4

View File

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/cloudsonic/sonic-server/conf"
"github.com/cloudsonic/sonic-server/persistence"
)
func main() {
@ -12,9 +11,7 @@ func main() {
fmt.Printf("\nCloudSonic Server v%s\n\n", "0.2")
provider := persistence.ProviderIdentifier(conf.Sonic.DevPersistenceProvider)
a := CreateApp(conf.Sonic.MusicFolder, provider)
a.MountRouter("/rest/", CreateSubsonicAPIRouter(provider))
a := CreateApp(conf.Sonic.MusicFolder)
a.MountRouter("/rest/", CreateSubsonicAPIRouter())
a.Run(":" + conf.Sonic.Port)
}

View File

@ -1 +0,0 @@
#-short

View File

@ -1,78 +0,0 @@
package db_ledis
import (
"errors"
"time"
"github.com/cloudsonic/sonic-server/domain"
)
type albumRepository struct {
ledisRepository
}
func NewAlbumRepository() domain.AlbumRepository {
r := &albumRepository{}
r.init("album", &domain.Album{})
return r
}
func (r *albumRepository) Put(m *domain.Album) error {
if m.ID == "" {
return errors.New("album ID is not set")
}
return r.saveOrUpdate(m.ID, m)
}
func (r *albumRepository) Get(id string) (*domain.Album, error) {
var rec interface{}
rec, err := r.readEntity(id)
return rec.(*domain.Album), err
}
func (r *albumRepository) FindByArtist(artistId string) (domain.Albums, error) {
var as = make(domain.Albums, 0)
err := r.loadChildren("artist", artistId, &as, domain.QueryOptions{SortBy: "Year"})
return as, err
}
func (r *albumRepository) GetAll(options ...domain.QueryOptions) (domain.Albums, error) {
var as = make(domain.Albums, 0)
err := r.loadAll(&as, options...)
return as, err
}
func (r *albumRepository) GetAllIds() ([]string, error) {
idMap, err := r.getAllIds()
if err != nil {
return nil, err
}
ids := make([]string, len(idMap))
i := 0
for id := range idMap {
ids[i] = id
i++
}
return ids, nil
}
func (r *albumRepository) PurgeInactive(active domain.Albums) ([]string, error) {
return r.purgeInactive(active, func(e interface{}) string {
return e.(domain.Album).ID
})
}
func (r *albumRepository) GetStarred(options ...domain.QueryOptions) (domain.Albums, error) {
var as = make(domain.Albums, 0)
start := time.Time{}.Add(1 * time.Hour)
err := r.loadRange("Starred", start, time.Now(), &as, options...)
return as, err
}
func (r *albumRepository) Search(q string, offset int, size int) (domain.Albums, error) {
return nil, errors.New("not implemented")
}
var _ domain.AlbumRepository = (*albumRepository)(nil)

View File

@ -1,41 +0,0 @@
package db_ledis
import (
"errors"
"github.com/cloudsonic/sonic-server/domain"
)
type artistRepository struct {
ledisRepository
}
func NewArtistRepository() domain.ArtistRepository {
r := &artistRepository{}
r.init("artist", &domain.Artist{})
return r
}
func (r *artistRepository) Put(m *domain.Artist) error {
if m.ID == "" {
return errors.New("artist ID is not set")
}
return r.saveOrUpdate(m.ID, m)
}
func (r *artistRepository) Get(id string) (*domain.Artist, error) {
var rec interface{}
rec, err := r.readEntity(id)
return rec.(*domain.Artist), err
}
func (r *artistRepository) PurgeInactive(active domain.Artists) ([]string, error) {
return r.purgeInactive(active, func(e interface{}) string {
return e.(domain.Artist).ID
})
}
func (r *artistRepository) Search(q string, offset int, size int) (domain.Artists, error) {
return nil, errors.New("not implemented")
}
var _ domain.ArtistRepository = (*artistRepository)(nil)

View File

@ -1,52 +0,0 @@
package db_ledis
import (
"github.com/cloudsonic/sonic-server/log"
"github.com/cloudsonic/sonic-server/scanner"
"github.com/siddontang/ledisdb/ledis"
)
var (
checkSumKeyName = []byte("checksums")
)
type checkSumRepository struct {
data map[string]string
}
func NewCheckSumRepository() scanner.CheckSumRepository {
r := &checkSumRepository{}
r.loadData()
return r
}
func (r *checkSumRepository) loadData() {
r.data = make(map[string]string)
pairs, err := Db().HGetAll(checkSumKeyName)
if err != nil {
log.Error("Error loading CheckSums", err)
}
for _, p := range pairs {
r.data[string(p.Field)] = string(p.Value)
}
log.Debug("Loaded checksums", "total", len(r.data))
}
func (r *checkSumRepository) Get(id string) (string, error) {
return r.data[id], nil
}
func (r *checkSumRepository) SetData(newSums map[string]string) error {
Db().HClear(checkSumKeyName)
pairs := make([]ledis.FVPair, len(newSums))
r.data = make(map[string]string)
i := 0
for id, sum := range newSums {
p := ledis.FVPair{Field: []byte(id), Value: []byte(sum)}
pairs[i] = p
r.data[id] = sum
i++
}
return Db().HMset(checkSumKeyName, pairs...)
}

View File

@ -1,49 +0,0 @@
package db_ledis
import (
"errors"
"sort"
"github.com/cloudsonic/sonic-server/domain"
)
type artistIndexRepository struct {
ledisRepository
}
func NewArtistIndexRepository() domain.ArtistIndexRepository {
r := &artistIndexRepository{}
r.init("index", &domain.ArtistIndex{})
return r
}
func (r *artistIndexRepository) Put(m *domain.ArtistIndex) error {
if m.ID == "" {
return errors.New("index ID is not set")
}
sort.Sort(m.Artists)
return r.saveOrUpdate(m.ID, m)
}
func (r *artistIndexRepository) Get(id string) (*domain.ArtistIndex, error) {
var rec interface{}
rec, err := r.readEntity(id)
return rec.(*domain.ArtistIndex), err
}
func (r *artistIndexRepository) GetAll() (domain.ArtistIndexes, error) {
var indices = make(domain.ArtistIndexes, 0)
err := r.loadAll(&indices, domain.QueryOptions{Alpha: true})
return indices, err
}
func (r *artistIndexRepository) DeleteAll() error {
var empty domain.ArtistIndexes
_, err := r.purgeInactive(empty, func(e interface{}) string {
return e.(domain.ArtistIndex).ID
})
return err
}
var _ domain.ArtistIndexRepository = (*artistIndexRepository)(nil)

View File

@ -1,70 +0,0 @@
package db_ledis
import (
"strconv"
"testing"
"github.com/cloudsonic/sonic-server/domain"
"github.com/cloudsonic/sonic-server/tests"
. "github.com/smartystreets/goconvey/convey"
)
func TestIndexRepository(t *testing.T) {
tests.Init(t, false)
Convey("Subject: NewIndexRepository", t, func() {
repo := NewArtistIndexRepository()
Convey("It should be able to read and write to the database", func() {
i := &domain.ArtistIndex{ID: "123"}
repo.Put(i)
s, _ := repo.Get("123")
So(s, shouldBeEqual, i)
})
Convey("It should be able to check for existence of an ID", func() {
i := &domain.ArtistIndex{ID: "123"}
repo.Put(i)
s, _ := repo.Exists("123")
So(s, ShouldBeTrue)
s, _ = repo.Exists("NOT_FOUND")
So(s, ShouldBeFalse)
})
Convey("Method Put() should return error if ID is not set", func() {
i := &domain.ArtistIndex{}
err := repo.Put(i)
So(err, ShouldNotBeNil)
})
Convey("Given that I have 4 records", func() {
for i := 1; i <= 4; i++ {
e := &domain.ArtistIndex{ID: strconv.Itoa(i)}
repo.Put(e)
}
Convey("When I call GetAll()", func() {
indices, err := repo.GetAll()
Convey("Then It should not return any error", func() {
So(err, ShouldBeNil)
})
Convey("And It should return 4 entities", func() {
So(indices, ShouldHaveLength, 4)
})
Convey("And the values should be retrieved", func() {
for _, e := range indices {
So(e.ID, ShouldBeIn, []string{"1", "2", "3", "4"})
}
})
})
})
Reset(func() {
dropDb()
})
})
}

View File

@ -1,37 +0,0 @@
package db_ledis
import (
"sync"
"github.com/cloudsonic/sonic-server/conf"
"github.com/cloudsonic/sonic-server/log"
"github.com/siddontang/ledisdb/config"
"github.com/siddontang/ledisdb/ledis"
)
var (
_ledisInstance *ledis.Ledis
_dbInstance *ledis.DB
once sync.Once
)
func Db() *ledis.DB {
once.Do(func() {
config := config.NewConfigDefault()
config.DataDir = conf.Sonic.DbPath
log.Debug("Opening LedisDB from: " + conf.Sonic.DbPath)
l, _ := ledis.Open(config)
instance, err := l.Select(0)
if err != nil {
panic(err)
}
_ledisInstance = l
_dbInstance = instance
})
return _dbInstance
}
func dropDb() {
Db()
_ledisInstance.FlushAll()
}

View File

@ -1,355 +0,0 @@
package db_ledis
import (
"crypto/md5"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"github.com/cloudsonic/sonic-server/domain"
"github.com/cloudsonic/sonic-server/utils"
"github.com/siddontang/ledisdb/ledis"
)
type ledisRepository struct {
table string
entityType reflect.Type
fieldNames []string
parentTable string
parentIdField string
indexes map[string]string
}
func (r *ledisRepository) init(table string, entity interface{}) {
r.table = table
r.entityType = reflect.TypeOf(entity).Elem()
h, _ := toMap(entity)
r.fieldNames = make([]string, len(h))
i := 0
for k := range h {
r.fieldNames[i] = k
i++
}
r.parseAnnotations(entity)
}
func (r *ledisRepository) parseAnnotations(entity interface{}) {
r.indexes = make(map[string]string)
dt := reflect.TypeOf(entity).Elem()
for i := 0; i < dt.NumField(); i++ {
f := dt.Field(i)
table := f.Tag.Get("parent")
if table != "" {
r.parentTable = table
r.parentIdField = f.Name
}
idx := f.Tag.Get("idx")
if idx != "" {
r.indexes[idx] = f.Name
}
}
}
// TODO Use annotations to specify fields to be used
func (r *ledisRepository) newId(fields ...string) string {
s := fmt.Sprintf("%s\\%s", strings.ToUpper(r.table), strings.Join(fields, ""))
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}
func (r *ledisRepository) CountAll() (int64, error) {
size, err := Db().ZCard([]byte(r.table + "s:all"))
return size, err
}
func (r *ledisRepository) getAllIds() (map[string]bool, error) {
m := make(map[string]bool)
pairs, err := Db().ZRange([]byte(r.table+"s:all"), 0, -1)
if err != nil {
return m, err
}
for _, p := range pairs {
m[string(p.Member)] = true
}
return m, err
}
type getIdFunc func(e interface{}) string
func (r *ledisRepository) purgeInactive(activeList interface{}, getId getIdFunc) ([]string, error) {
currentIds, err := r.getAllIds()
if err != nil {
return nil, err
}
reflected := reflect.ValueOf(activeList)
totalActive := reflected.Len()
for i := 0; i < totalActive; i++ {
a := reflected.Index(i)
id := getId(a.Interface())
currentIds[id] = false
}
inactiveIds := make([]string, 0, len(currentIds)-totalActive)
for id, inactive := range currentIds {
if inactive {
inactiveIds = append(inactiveIds, id)
}
}
return inactiveIds, r.removeAll(inactiveIds)
}
func (r *ledisRepository) removeAll(ids []string) error {
allKey := r.table + "s:all"
keys := make([][]byte, len(ids))
i := 0
for _, id := range ids {
// Delete from parent:parentId:table (ZSet)
if r.parentTable != "" {
parentKey := []byte(fmt.Sprintf("%s:%s:%s", r.table, id, r.parentIdField))
pid, err := Db().Get(parentKey)
var parentId string
if err := json.Unmarshal(pid, &parentId); err != nil {
return err
}
if err != nil {
return err
}
parentKey = []byte(fmt.Sprintf("%s:%s:%ss", r.parentTable, parentId, r.table))
if _, err := Db().ZRem(parentKey, []byte(id)); err != nil {
return err
}
}
// Delete table:idx:* (ZSet)
for idx := range r.indexes {
idxName := fmt.Sprintf("%s:idx:%s", r.table, idx)
if _, err := Db().ZRem([]byte(idxName), []byte(id)); err != nil {
return err
}
}
// Delete record table:id:* (KV)
if err := r.deleteRecord(id); err != nil {
return err
}
keys[i] = []byte(id)
i++
}
// Delete from table:all (ZSet)
_, err := Db().ZRem([]byte(allKey), keys...)
return err
}
func (r *ledisRepository) deleteRecord(id string) error {
keys := r.getFieldKeys(id)
_, err := Db().Del(keys...)
return err
}
func (r *ledisRepository) Exists(id string) (bool, error) {
res, _ := Db().ZScore([]byte(r.table+"s:all"), []byte(id))
return res != ledis.InvalidScore, nil
}
func (r *ledisRepository) saveOrUpdate(id string, entity interface{}) error {
recordPrefix := fmt.Sprintf("%s:%s:", r.table, id)
allKey := r.table + "s:all"
h, err := toMap(entity)
if err != nil {
return err
}
for f, v := range h {
key := recordPrefix + f
value, _ := json.Marshal(v)
if err := Db().Set([]byte(key), value); err != nil {
return err
}
}
for idx, fn := range r.indexes {
idxName := fmt.Sprintf("%s:idx:%s", r.table, idx)
score := calcScore(entity, fn)
sidx := ledis.ScorePair{Score: score, Member: []byte(id)}
if _, err = Db().ZAdd([]byte(idxName), sidx); err != nil {
return err
}
}
sid := ledis.ScorePair{Score: 0, Member: []byte(id)}
if _, err = Db().ZAdd([]byte(allKey), sid); err != nil {
return err
}
if parentCollectionKey := r.getParentRelationKey(entity); parentCollectionKey != "" {
_, err = Db().ZAdd([]byte(parentCollectionKey), sid)
}
return err
}
func calcScore(entity interface{}, fieldName string) int64 {
dv := reflect.ValueOf(entity).Elem()
v := dv.FieldByName(fieldName)
return toScore(v.Interface())
}
func toScore(value interface{}) int64 {
switch v := value.(type) {
case int:
return int64(v)
case bool:
if v {
return 1
}
case time.Time:
return utils.ToMillis(v)
default:
panic("Not implemented")
}
return 0
}
func (r *ledisRepository) getParentRelationKey(entity interface{}) string {
parentId := r.getParentId(entity)
if parentId != "" {
return fmt.Sprintf("%s:%s:%ss", r.parentTable, parentId, r.table)
}
return ""
}
func (r *ledisRepository) getParentId(entity interface{}) string {
if r.parentTable != "" {
dv := reflect.ValueOf(entity).Elem()
return dv.FieldByName(r.parentIdField).String()
}
return ""
}
func (r *ledisRepository) getFieldKeys(id string) [][]byte {
recordPrefix := fmt.Sprintf("%s:%s:", r.table, id)
var fieldKeys = make([][]byte, len(r.fieldNames))
for i, n := range r.fieldNames {
fieldKeys[i] = []byte(recordPrefix + n)
}
return fieldKeys
}
func (r *ledisRepository) newInstance() interface{} {
return reflect.New(r.entityType).Interface()
}
func (r *ledisRepository) readEntity(id string) (interface{}, error) {
entity := r.newInstance()
fieldKeys := r.getFieldKeys(id)
res, err := Db().MGet(fieldKeys...)
if err != nil {
return nil, err
}
if len(res[0]) == 0 {
return entity, domain.ErrNotFound
}
err = r.toEntity(res, entity)
return entity, err
}
func (r *ledisRepository) toEntity(response [][]byte, entity interface{}) error {
var record = make(map[string]interface{}, len(response))
for i, v := range response {
if len(v) > 0 {
var value interface{}
if err := json.Unmarshal(v, &value); err != nil {
return err
}
record[string(r.fieldNames[i])] = value
}
}
return toStruct(record, entity)
}
func (r *ledisRepository) loadRange(idxName string, min interface{}, max interface{}, entities interface{}, qo ...domain.QueryOptions) error {
o := domain.QueryOptions{}
if len(qo) > 0 {
o = qo[0]
}
if o.Size == 0 {
o.Size = -1
}
minS := toScore(min)
maxS := toScore(max)
idxKey := fmt.Sprintf("%s:idx:%s", r.table, idxName)
var resp []ledis.ScorePair
var err error
if o.Desc {
resp, err = Db().ZRevRangeByScore([]byte(idxKey), minS, maxS, o.Offset, o.Size)
} else {
resp, err = Db().ZRangeByScore([]byte(idxKey), minS, maxS, o.Offset, o.Size)
}
if err != nil {
return err
}
reflected := reflect.ValueOf(entities).Elem()
for _, pair := range resp {
e, err := r.readEntity(string(pair.Member))
if err != nil {
return err
}
reflected.Set(reflect.Append(reflected, reflect.ValueOf(e).Elem()))
}
return nil
}
func (r *ledisRepository) loadAll(entities interface{}, qo ...domain.QueryOptions) error {
setName := r.table + "s:all"
return r.loadFromSet(setName, entities, qo...)
}
func (r *ledisRepository) loadChildren(parentTable string, parentId string, emptyEntityArray interface{}, qo ...domain.QueryOptions) error {
setName := fmt.Sprintf("%s:%s:%ss", parentTable, parentId, r.table)
return r.loadFromSet(setName, emptyEntityArray, qo...)
}
// TODO Optimize it! Probably very slow (and confusing!)
func (r *ledisRepository) loadFromSet(setName string, entities interface{}, qo ...domain.QueryOptions) error {
o := domain.QueryOptions{}
if len(qo) > 0 {
o = qo[0]
}
reflected := reflect.ValueOf(entities).Elem()
var sortKey []byte
if o.SortBy != "" {
sortKey = []byte(fmt.Sprintf("%s:*:%s", r.table, o.SortBy))
}
response, err := Db().XZSort([]byte(setName), o.Offset, o.Size, o.Alpha, o.Desc, sortKey, r.getFieldKeys("*"))
if err != nil {
return err
}
numFields := len(r.fieldNames)
for i := 0; i < (len(response) / numFields); i++ {
start := i * numFields
entity := reflect.New(r.entityType).Interface()
if err := r.toEntity(response[start:start+numFields], entity); err != nil {
return err
}
reflected.Set(reflect.Append(reflected, reflect.ValueOf(entity).Elem()))
}
return nil
}

View File

@ -1,256 +0,0 @@
package db_ledis
import (
"fmt"
"strconv"
"testing"
"time"
"github.com/cloudsonic/sonic-server/domain"
"github.com/cloudsonic/sonic-server/tests"
"github.com/cloudsonic/sonic-server/utils"
. "github.com/smartystreets/goconvey/convey"
)
type TestEntity struct {
Id string
Name string
ParentId string `parent:"parent"`
Year time.Time `idx:"ByYear"`
Count int `idx:"ByCount"`
Flag bool `idx:"ByFlag"`
}
func shouldBeEqual(actualStruct interface{}, expectedStruct ...interface{}) string {
actual := fmt.Sprintf("%v", actualStruct)
expected := fmt.Sprintf("%v", expectedStruct[0])
return ShouldEqual(actual, expected)
}
func createEmptyRepo() *ledisRepository {
dropDb()
repo := &ledisRepository{}
repo.init("test", &TestEntity{})
return repo
}
func TestBaseRepository(t *testing.T) {
tests.Init(t, false)
Convey("Subject: Annotations", t, func() {
repo := createEmptyRepo()
Convey("It should parse the parent table definition", func() {
So(repo.parentTable, ShouldEqual, "parent")
So(repo.parentIdField, ShouldEqual, "ParentId")
})
Convey("It should parse the definded indexes", func() {
So(repo.indexes, ShouldHaveLength, 3)
So(repo.indexes["ByYear"], ShouldEqual, "Year")
So(repo.indexes["ByFlag"], ShouldEqual, "Flag")
So(repo.indexes["ByCount"], ShouldEqual, "Count")
})
})
Convey("Subject: calcScore", t, func() {
repo := createEmptyRepo()
Convey("It should create an int score", func() {
def := repo.indexes["ByCount"]
entity := &TestEntity{Count: 10}
score := calcScore(entity, def)
So(score, ShouldEqual, 10)
})
Convey("It should create a boolean score", func() {
def := repo.indexes["ByFlag"]
Convey("Value false", func() {
entity := &TestEntity{Flag: false}
score := calcScore(entity, def)
So(score, ShouldEqual, 0)
})
Convey("Value true", func() {
entity := &TestEntity{Flag: true}
score := calcScore(entity, def)
So(score, ShouldEqual, 1)
})
})
Convey("It should create a time score", func() {
def := repo.indexes["ByYear"]
now := time.Now()
entity := &TestEntity{Year: now}
score := calcScore(entity, def)
So(score, ShouldEqual, utils.ToMillis(now))
})
})
Convey("Subject: NewId", t, func() {
repo := createEmptyRepo()
Convey("When I call NewId with a name", func() {
Id := repo.newId("a name")
Convey("Then it should return a new ID", func() {
So(Id, ShouldNotBeEmpty)
})
})
Convey("When I call NewId with the same name twice", func() {
FirstId := repo.newId("a name")
SecondId := repo.newId("a name")
Convey("Then it should return the same ID each time", func() {
So(FirstId, ShouldEqual, SecondId)
})
})
Convey("When I call NewId with different names", func() {
FirstId := repo.newId("first name")
SecondId := repo.newId("second name")
Convey("Then it should return different Ids", func() {
So(FirstId, ShouldNotEqual, SecondId)
})
})
})
Convey("Subject: saveOrUpdate/loadEntity/CountAll", t, func() {
Convey("Given an empty DB", func() {
repo := createEmptyRepo()
Convey("When I try to retrieve an nonexistent ID", func() {
_, err := repo.readEntity("NOT_FOUND")
Convey("Then I should get a NotFound error", func() {
So(err, ShouldEqual, domain.ErrNotFound)
})
})
Convey("When I save a new entity and a parent", func() {
entity := &TestEntity{Id: "123", Name: "My Name", ParentId: "ABC", Year: time.Time{}}
err := repo.saveOrUpdate("123", entity)
Convey("Then saving the entity shouldn't return any errors", func() {
So(err, ShouldBeNil)
})
Convey("And the number of entities should be 1", func() {
count, _ := repo.CountAll()
So(count, ShouldEqual, 1)
})
Convey("And the number of children should be 1", func() {
var children []TestEntity
err := repo.loadChildren("parent", "ABC", &children)
So(err, ShouldBeNil)
So(len(children), ShouldEqual, 1)
})
Convey("And this entity should be equal to the the saved one", func() {
actualEntity, _ := repo.readEntity("123")
So(actualEntity, shouldBeEqual, entity)
})
})
})
Convey("Given a table with one entity", func() {
repo := createEmptyRepo()
entity := &TestEntity{Id: "111", Name: "One Name", ParentId: "AAA"}
repo.saveOrUpdate(entity.Id, entity)
Convey("When I save an entity with a different ID", func() {
newEntity := &TestEntity{Id: "222", Name: "Another Name", ParentId: "AAA"}
repo.saveOrUpdate(newEntity.Id, newEntity)
Convey("Then the number of entities should be 2", func() {
count, _ := repo.CountAll()
So(count, ShouldEqual, 2)
})
})
Convey("When I save an entity with the same ID", func() {
newEntity := &TestEntity{Id: "111", Name: "New Name", ParentId: "AAA"}
repo.saveOrUpdate(newEntity.Id, newEntity)
Convey("Then the number of entities should be 1", func() {
count, _ := repo.CountAll()
So(count, ShouldEqual, 1)
})
Convey("And the entity should be updated", func() {
e, _ := repo.readEntity("111")
actualEntity := e.(*TestEntity)
So(actualEntity.Name, ShouldEqual, newEntity.Name)
})
})
})
Convey("Given a table with 3 entities", func() {
repo := createEmptyRepo()
for i := 1; i <= 3; i++ {
e := &TestEntity{Id: strconv.Itoa(i), Name: fmt.Sprintf("Name %d", i), ParentId: "AAA"}
repo.saveOrUpdate(e.Id, e)
}
Convey("When I call loadAll", func() {
var es = make([]TestEntity, 0)
err := repo.loadAll(&es)
Convey("Then It should not return any error", func() {
So(err, ShouldBeNil)
})
Convey("And I should get 3 entities", func() {
So(len(es), ShouldEqual, 3)
})
Convey("And the values should be retrieved", func() {
for _, e := range es {
So(e.Id, ShouldBeIn, []string{"1", "2", "3"})
So(e.Name, ShouldBeIn, []string{"Name 1", "Name 2", "Name 3"})
So(e.ParentId, ShouldEqual, "AAA")
}
})
})
Convey("When I call GetAllIds", func() {
ids, err := repo.getAllIds()
Convey("Then It should not return any error", func() {
So(err, ShouldBeNil)
})
Convey("And I get all saved ids", func() {
So(len(ids), ShouldEqual, 3)
for k := range ids {
So(k, ShouldBeIn, []string{"1", "2", "3"})
}
})
})
Convey("When I call DeletaAll with one of the entities", func() {
ids := []string{"1"}
err := repo.removeAll(ids)
Convey("Then It should not return any error", func() {
So(err, ShouldBeNil)
})
Convey("Then CountAll should return 2", func() {
count, _ := repo.CountAll()
So(count, ShouldEqual, 2)
})
Convey("And the deleted record shouldn't be among the children", func() {
var children []TestEntity
err := repo.loadChildren("parent", "AAA", &children)
So(err, ShouldBeNil)
So(len(children), ShouldEqual, 2)
for _, e := range children {
So(e.Id, ShouldNotEqual, "1")
}
})
})
})
})
}

View File

@ -1,30 +0,0 @@
package db_ledis
import (
"encoding/json"
)
func toMap(rec interface{}) (map[string]interface{}, error) {
// Convert to JSON...
b, err := json.Marshal(rec)
if err != nil {
return nil, err
}
// ... then convert to map
var m map[string]interface{}
err = json.Unmarshal(b, &m)
return m, err
}
func toStruct(m map[string]interface{}, rec interface{}) error {
// Convert to JSON...
b, err := json.Marshal(m)
if err != nil {
return err
}
// ... then convert to struct
err = json.Unmarshal(b, &rec)
return err
}

View File

@ -1,80 +0,0 @@
package db_ledis
import (
"errors"
"sort"
"time"
"github.com/cloudsonic/sonic-server/domain"
)
type mediaFileRepository struct {
ledisRepository
}
func NewMediaFileRepository() domain.MediaFileRepository {
r := &mediaFileRepository{}
r.init("mediafile", &domain.MediaFile{})
return r
}
func (r *mediaFileRepository) Put(m *domain.MediaFile) error {
if m.ID == "" {
return errors.New("mediaFile ID is not set")
}
return r.saveOrUpdate(m.ID, m)
}
func (r *mediaFileRepository) Get(id string) (*domain.MediaFile, error) {
m, err := r.readEntity(id)
if err != nil {
return nil, err
}
mf := m.(*domain.MediaFile)
if mf.ID != id {
return nil, nil
}
return mf, nil
}
func (r *mediaFileRepository) FindByAlbum(albumId string) (domain.MediaFiles, error) {
var mfs = make(domain.MediaFiles, 0)
err := r.loadChildren("album", albumId, &mfs, domain.QueryOptions{SortBy: "TrackNumber"})
sort.Sort(mfs)
return mfs, err
}
func (r *mediaFileRepository) GetStarred(options ...domain.QueryOptions) (domain.MediaFiles, error) {
var mfs = make(domain.MediaFiles, 0)
start := time.Time{}.Add(1 * time.Hour)
err := r.loadRange("Starred", start, time.Now(), &mfs, options...)
return mfs, err
}
func (r *mediaFileRepository) GetAllIds() ([]string, error) {
idMap, err := r.getAllIds()
if err != nil {
return nil, err
}
ids := make([]string, len(idMap))
i := 0
for id := range idMap {
ids[i] = id
i++
}
return ids, nil
}
func (r *mediaFileRepository) PurgeInactive(active domain.MediaFiles) ([]string, error) {
return r.purgeInactive(active, func(e interface{}) string {
return e.(domain.MediaFile).ID
})
}
func (r *mediaFileRepository) Search(q string, offset int, size int) (domain.MediaFiles, error) {
return nil, errors.New("not implemented")
}
var _ domain.MediaFileRepository = (*mediaFileRepository)(nil)

View File

@ -1,97 +0,0 @@
package db_ledis
import (
"encoding/json"
"fmt"
"github.com/cloudsonic/sonic-server/domain"
)
var (
nowPlayingKeyPrefix = []byte("nowplaying")
)
type nowPlayingRepository struct {
ledisRepository
}
func NewNowPlayingRepository() domain.NowPlayingRepository {
r := &nowPlayingRepository{}
r.init("nowplaying", &domain.NowPlayingInfo{})
return r
}
func nowPlayingKeyName(playerId int) string {
return fmt.Sprintf("%s:%d", nowPlayingKeyPrefix, playerId)
}
func (r *nowPlayingRepository) Enqueue(info *domain.NowPlayingInfo) error {
h, err := json.Marshal(info)
if err != nil {
return err
}
keyName := []byte(nowPlayingKeyName(info.PlayerId))
_, err = Db().LPush(keyName, []byte(h))
Db().LExpire(keyName, int64(domain.NowPlayingExpire.Seconds()))
return err
}
func (r *nowPlayingRepository) Dequeue(playerId int) (*domain.NowPlayingInfo, error) {
keyName := []byte(nowPlayingKeyName(playerId))
val, err := Db().RPop(keyName)
if err != nil {
return nil, err
}
if val == nil {
return nil, nil
}
return r.parseInfo(val)
}
func (r *nowPlayingRepository) Head(playerId int) (*domain.NowPlayingInfo, error) {
keyName := []byte(nowPlayingKeyName(playerId))
val, err := Db().LIndex(keyName, 0)
if err != nil {
return nil, err
}
return r.parseInfo(val)
}
func (r *nowPlayingRepository) Tail(playerId int) (*domain.NowPlayingInfo, error) {
keyName := []byte(nowPlayingKeyName(playerId))
val, err := Db().LIndex(keyName, -1)
if err != nil {
return nil, err
}
return r.parseInfo(val)
}
func (r *nowPlayingRepository) Count(playerId int) (int64, error) {
keyName := []byte(nowPlayingKeyName(playerId))
return Db().LLen(keyName)
}
// TODO Will not work for multiple players
func (r *nowPlayingRepository) GetAll() ([]*domain.NowPlayingInfo, error) {
np, err := r.Head(1)
if np == nil || err != nil {
return nil, err
}
return []*domain.NowPlayingInfo{np}, err
}
func (r *nowPlayingRepository) parseInfo(val []byte) (*domain.NowPlayingInfo, error) {
info := &domain.NowPlayingInfo{}
err := json.Unmarshal(val, info)
if err != nil {
return nil, nil
}
return info, nil
}
var _ domain.NowPlayingRepository = (*nowPlayingRepository)(nil)

View File

@ -1,52 +0,0 @@
package db_ledis
import (
"errors"
"github.com/cloudsonic/sonic-server/domain"
)
type playlistRepository struct {
ledisRepository
}
func NewPlaylistRepository() domain.PlaylistRepository {
r := &playlistRepository{}
r.init("playlist", &domain.Playlist{})
return r
}
func (r *playlistRepository) Put(m *domain.Playlist) error {
if m.ID == "" {
return errors.New("playlist ID is not set")
}
return r.saveOrUpdate(m.ID, m)
}
func (r *playlistRepository) Get(id string) (*domain.Playlist, error) {
var rec interface{}
rec, err := r.readEntity(id)
return rec.(*domain.Playlist), err
}
func (r *playlistRepository) GetAll(options ...domain.QueryOptions) (domain.Playlists, error) {
o := domain.QueryOptions{}
if len(options) > 0 {
o = options[0]
}
var as = make(domain.Playlists, 0)
if o.SortBy == "" {
o.SortBy = "Name"
o.Alpha = true
}
err := r.loadAll(&as, o)
return as, err
}
func (r *playlistRepository) PurgeInactive(active domain.Playlists) ([]string, error) {
return r.purgeInactive(active, func(e interface{}) string {
return e.(domain.Playlist).ID
})
}
var _ domain.PlaylistRepository = (*playlistRepository)(nil)

View File

@ -1,43 +0,0 @@
package db_ledis
import (
"errors"
"github.com/cloudsonic/sonic-server/domain"
)
type propertyRepository struct {
ledisRepository
}
func NewPropertyRepository() domain.PropertyRepository {
r := &propertyRepository{}
r.init("property", &domain.Property{})
return r
}
func (r *propertyRepository) Put(id string, value string) error {
m := &domain.Property{ID: id, Value: value}
if m.ID == "" {
return errors.New("ID is required")
}
return r.saveOrUpdate(m.ID, m)
}
func (r *propertyRepository) Get(id string) (string, error) {
var rec interface{}
rec, err := r.readEntity(id)
return rec.(*domain.Property).Value, err
}
func (r *propertyRepository) DefaultGet(id string, defaultValue string) (string, error) {
v, err := r.Get(id)
if err == domain.ErrNotFound {
return defaultValue, nil
}
return v, err
}
var _ domain.PropertyRepository = (*propertyRepository)(nil)

View File

@ -1,19 +0,0 @@
package db_ledis
import (
"github.com/cloudsonic/sonic-server/persistence"
"github.com/google/wire"
)
var Set = wire.NewSet(
NewPropertyRepository,
NewArtistRepository,
NewAlbumRepository,
NewMediaFileRepository,
NewArtistIndexRepository,
NewPlaylistRepository,
NewCheckSumRepository,
NewNowPlayingRepository,
persistence.NewMediaFolderRepository,
wire.Value(persistence.ProviderIdentifier("levisdb")),
)

View File

@ -11,7 +11,6 @@ import (
"github.com/cloudsonic/sonic-server/engine"
"github.com/cloudsonic/sonic-server/itunesbridge"
"github.com/cloudsonic/sonic-server/persistence"
"github.com/cloudsonic/sonic-server/persistence/db_ledis"
"github.com/cloudsonic/sonic-server/persistence/db_sql"
"github.com/cloudsonic/sonic-server/scanner"
"github.com/google/wire"
@ -19,8 +18,8 @@ import (
// Injectors from wire_injectors.go:
func CreateApp(musicFolder string, p persistence.ProviderIdentifier) *App {
provider := createPersistenceProvider(p)
func CreateApp(musicFolder string) *App {
provider := createPersistenceProvider()
checkSumRepository := provider.CheckSumRepository
itunesScanner := scanner.NewItunesScanner(checkSumRepository)
mediaFileRepository := provider.MediaFileRepository
@ -34,8 +33,8 @@ func CreateApp(musicFolder string, p persistence.ProviderIdentifier) *App {
return app
}
func CreateSubsonicAPIRouter(p persistence.ProviderIdentifier) *api.Router {
provider := createPersistenceProvider(p)
func CreateSubsonicAPIRouter() *api.Router {
provider := createPersistenceProvider()
propertyRepository := provider.PropertyRepository
mediaFolderRepository := provider.MediaFolderRepository
artistIndexRepository := provider.ArtistIndexRepository
@ -56,7 +55,7 @@ func CreateSubsonicAPIRouter(p persistence.ProviderIdentifier) *api.Router {
return router
}
func createSQLProvider() *Provider {
func createPersistenceProvider() *Provider {
albumRepository := db_sql.NewAlbumRepository()
artistRepository := db_sql.NewArtistRepository()
checkSumRepository := db_sql.NewCheckSumRepository()
@ -80,30 +79,6 @@ func createSQLProvider() *Provider {
return provider
}
func createLedisDBProvider() *Provider {
albumRepository := db_ledis.NewAlbumRepository()
artistRepository := db_ledis.NewArtistRepository()
checkSumRepository := db_ledis.NewCheckSumRepository()
artistIndexRepository := db_ledis.NewArtistIndexRepository()
mediaFileRepository := db_ledis.NewMediaFileRepository()
mediaFolderRepository := persistence.NewMediaFolderRepository()
nowPlayingRepository := db_ledis.NewNowPlayingRepository()
playlistRepository := db_ledis.NewPlaylistRepository()
propertyRepository := db_ledis.NewPropertyRepository()
provider := &Provider{
AlbumRepository: albumRepository,
ArtistRepository: artistRepository,
CheckSumRepository: checkSumRepository,
ArtistIndexRepository: artistIndexRepository,
MediaFileRepository: mediaFileRepository,
MediaFolderRepository: mediaFolderRepository,
NowPlayingRepository: nowPlayingRepository,
PlaylistRepository: playlistRepository,
PropertyRepository: propertyRepository,
}
return provider
}
// wire_injectors.go:
type Provider struct {
@ -122,12 +97,3 @@ var allProviders = wire.NewSet(itunesbridge.NewItunesControl, engine.Set, scanne
"ArtistIndexRepository", "MediaFileRepository", "MediaFolderRepository", "NowPlayingRepository",
"PlaylistRepository", "PropertyRepository"), createPersistenceProvider,
)
func createPersistenceProvider(provider persistence.ProviderIdentifier) *Provider {
switch provider {
case "sql":
return createSQLProvider()
default:
return createLedisDBProvider()
}
}

View File

@ -7,8 +7,6 @@ import (
"github.com/cloudsonic/sonic-server/domain"
"github.com/cloudsonic/sonic-server/engine"
"github.com/cloudsonic/sonic-server/itunesbridge"
"github.com/cloudsonic/sonic-server/persistence"
"github.com/cloudsonic/sonic-server/persistence/db_ledis"
"github.com/cloudsonic/sonic-server/persistence/db_sql"
"github.com/cloudsonic/sonic-server/scanner"
"github.com/google/wire"
@ -37,36 +35,22 @@ var allProviders = wire.NewSet(
createPersistenceProvider,
)
func CreateApp(musicFolder string, p persistence.ProviderIdentifier) *App {
func CreateApp(musicFolder string) *App {
panic(wire.Build(
NewApp,
allProviders,
))
}
func CreateSubsonicAPIRouter(p persistence.ProviderIdentifier) *api.Router {
func CreateSubsonicAPIRouter() *api.Router {
panic(wire.Build(allProviders))
}
func createPersistenceProvider(provider persistence.ProviderIdentifier) *Provider {
switch provider {
case "sql":
return createSQLProvider()
default:
return createLedisDBProvider()
}
}
func createSQLProvider() *Provider {
// When implementing a different persistence layer, duplicate this function (in separated files) and use build tags
// to conditionally select which function to use
func createPersistenceProvider() *Provider {
panic(wire.Build(
db_sql.Set,
wire.Struct(new(Provider), "*"),
))
}
func createLedisDBProvider() *Provider {
panic(wire.Build(
db_ledis.Set,
wire.Struct(new(Provider), "*"),
))
}