mirror of
https://github.com/safing/portbase
synced 2025-09-01 10:09:50 +00:00
Finalize model
This commit is contained in:
parent
94598b115b
commit
9b7365376c
13 changed files with 320 additions and 100 deletions
38
database/controller.go
Normal file
38
database/controller.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package database
|
||||
|
||||
type Controller struct {
|
||||
storage
|
||||
writeLock sync.RWMutex
|
||||
readLock sync.RWMutex
|
||||
migrating *abool.AtomicBool
|
||||
}
|
||||
|
||||
func NewController() (*Controller, error) {
|
||||
|
||||
}
|
||||
|
||||
// Retrieve
|
||||
func (c *Controller) Exists(key string) (bool, error) {}
|
||||
func (c *Controller) Get(key string) (model.Model, error) {}
|
||||
|
||||
// Modify
|
||||
func (c *Controller) Create(model model.Model) error {}
|
||||
// create when not exists
|
||||
func (c *Controller) Update(model model.Model) error {}
|
||||
// update, create if not exists.
|
||||
func (c *Controller) UpdateOrCreate(model model.Model) error {}
|
||||
func (c *Controller) Delete(key string) error {}
|
||||
|
||||
// Partial
|
||||
// What happens if I mutate a value that does not yet exist? How would I know its type?
|
||||
func (c *Controller) InsertPartial(key string, partialObject interface{}) {}
|
||||
func (c *Controller) InsertValue(key string, attribute string, value interface{}) {}
|
||||
|
||||
// Query
|
||||
func (c *Controller) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {}
|
||||
|
||||
// Meta
|
||||
func (c *Controller) SetAbsoluteExpiry(key string, time int64) {}
|
||||
func (c *Controller) SetRelativateExpiry(key string, duration int64) {}
|
||||
func (c *Controller) MakeCrownJewel(key string) {}
|
||||
func (c *Controller) MakeSecret(key string) {}
|
|
@ -2,11 +2,11 @@ package database
|
|||
|
||||
|
||||
var (
|
||||
databases = make(map[string]*storage.Interface)
|
||||
databases = make(map[string]*Controller)
|
||||
databasesLock sync.Mutex
|
||||
)
|
||||
|
||||
func getDatabase(name string) *storage.Interface {
|
||||
func getDatabase(name string) *Controller {
|
||||
databasesLock.Lock()
|
||||
defer databasesLock.Unlock()
|
||||
storage, ok := databases[name]
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
package database
|
||||
|
||||
type Interface struct {
|
||||
local bool
|
||||
internal bool
|
||||
// Interface provides a method to access the database with attached options.
|
||||
type Interface struct {}
|
||||
|
||||
// Options holds options that may be set for an Interface instance.
|
||||
type Options struct {
|
||||
Local bool
|
||||
Internal bool
|
||||
AlwaysMakeSecret bool
|
||||
AlwaysMakeCrownjewel bool
|
||||
}
|
||||
|
||||
func NewInterface(local bool, internal bool) *Interface {
|
||||
// NewInterface returns a new Interface to the database.
|
||||
func NewInterface(opts *Options) *Interface {
|
||||
return &Interface{
|
||||
local: local,
|
||||
internal: internal,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interface) Get(string key) (model.Model, error) {
|
||||
func (i *Interface) Get(key string) (model.Model, error) {
|
||||
|
||||
controller
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Safing/portbase/container"
|
||||
"github.com/Safing/portbase/formats/dsd"
|
||||
)
|
||||
|
||||
|
@ -13,17 +17,27 @@ type Base struct {
|
|||
|
||||
// Key returns the key of the database record.
|
||||
func (b *Base) Key() string {
|
||||
return fmt.Sprintf("%s:%s", b.dbName, b.dbKey)
|
||||
}
|
||||
|
||||
// DatabaseName returns the name of the database.
|
||||
func (b *Base) DatabaseName() string {
|
||||
return b.dbName
|
||||
}
|
||||
|
||||
// DatabaseKey returns the database key of the database record.
|
||||
func (b *Base) DatabaseKey() string {
|
||||
return b.dbKey
|
||||
}
|
||||
|
||||
// SetKey sets the key on the database record, it should only be called after loading the record. Use MoveTo to save the record with another key.
|
||||
func (b *Base) SetKey(key string) {
|
||||
b.dbKey = key
|
||||
b.dbName, b.dbKey = ParseKey(key)
|
||||
}
|
||||
|
||||
// MoveTo sets a new key for the record and resets all metadata, except for the secret and crownjewel status.
|
||||
func (b *Base) MoveTo(key string) {
|
||||
b.dbKey = key
|
||||
b.SetKey(key)
|
||||
b.meta.Reset()
|
||||
}
|
||||
|
||||
|
@ -45,3 +59,29 @@ func (b *Base) Marshal(format uint8) ([]byte, error) {
|
|||
}
|
||||
return dumped, nil
|
||||
}
|
||||
|
||||
// MarshalRecord packs the object, including metadata, into a byte array for saving in a database.
|
||||
func (b *Base) MarshalRecord() ([]byte, error) {
|
||||
if b.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
}
|
||||
|
||||
// version
|
||||
c := container.New([]byte{1})
|
||||
|
||||
// meta
|
||||
metaSection, err := b.meta.GenCodeMarshal(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.AppendAsBlock(metaSection)
|
||||
|
||||
// data
|
||||
dataSection, err := b.Marshal(dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Append(dataSection)
|
||||
|
||||
return c.CompileData(), nil
|
||||
}
|
||||
|
|
13
database/model/base_test.go
Normal file
13
database/model/base_test.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBaseModel(t *testing.T) {
|
||||
|
||||
// check model interface compliance
|
||||
var m Model
|
||||
b := &TestModel{}
|
||||
m = b
|
||||
_ = m
|
||||
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseKey(key string) (dbName, dbKey string, err error) {
|
||||
// ParseKey splits a key into it's database name and key parts.
|
||||
func ParseKey(key string) (dbName, dbKey string) {
|
||||
splitted := strings.SplitN(key, ":", 2)
|
||||
if len(splitted) == 2 {
|
||||
return splitted[0], splitted[1], nil
|
||||
return splitted[0], splitted[1]
|
||||
}
|
||||
return "", "", errors.New("invalid key")
|
||||
return splitted[0], ""
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
@ -13,7 +14,7 @@ var (
|
|||
)
|
||||
|
||||
// GenCodeSize returns the size of the gencode marshalled byte slice
|
||||
func (d *Meta) GenCodeSize() (s uint64) {
|
||||
func (d *Meta) GenCodeSize() (s int) {
|
||||
s += 34
|
||||
return
|
||||
}
|
||||
|
@ -22,7 +23,7 @@ func (d *Meta) GenCodeSize() (s uint64) {
|
|||
func (d *Meta) GenCodeMarshal(buf []byte) ([]byte, error) {
|
||||
size := d.GenCodeSize()
|
||||
{
|
||||
if uint64(cap(buf)) >= size {
|
||||
if cap(buf) >= size {
|
||||
buf = buf[:size]
|
||||
} else {
|
||||
buf = make([]byte, size)
|
||||
|
@ -125,6 +126,10 @@ func (d *Meta) GenCodeMarshal(buf []byte) ([]byte, error) {
|
|||
|
||||
// GenCodeUnmarshal gencode unmarshalls Meta and returns the bytes read.
|
||||
func (d *Meta) GenCodeUnmarshal(buf []byte) (uint64, error) {
|
||||
if len(buf) < d.GenCodeSize() {
|
||||
return 0, fmt.Errorf("insufficient data: got %d out of %d bytes", len(buf), d.GenCodeSize())
|
||||
}
|
||||
|
||||
i := uint64(0)
|
||||
|
||||
{
|
||||
|
|
|
@ -12,19 +12,37 @@ type Meta struct {
|
|||
cronjewel bool // crownjewels must never leave the instance, but may be read by the UI
|
||||
}
|
||||
|
||||
// SetAbsoluteExpiry sets an absolute expiry time, that is not affected when the record is updated.
|
||||
func (m *Meta) SetAbsoluteExpiry(time int64) {
|
||||
m.expires = time
|
||||
// SetAbsoluteExpiry sets an absolute expiry time (in seconds), that is not affected when the record is updated.
|
||||
func (m *Meta) SetAbsoluteExpiry(seconds int64) {
|
||||
m.expires = seconds
|
||||
m.deleted = 0
|
||||
}
|
||||
|
||||
// SetRelativateExpiry sets a relative expiry that is automatically updated whenever the record is updated/saved.
|
||||
func (m *Meta) SetRelativateExpiry(duration int64) {
|
||||
if duration >= 0 {
|
||||
m.deleted = -duration
|
||||
// SetRelativateExpiry sets a relative expiry time (ie. TTL in seconds) that is automatically updated whenever the record is updated/saved.
|
||||
func (m *Meta) SetRelativateExpiry(seconds int64) {
|
||||
if seconds >= 0 {
|
||||
m.deleted = -seconds
|
||||
}
|
||||
}
|
||||
|
||||
// GetAbsoluteExpiry returns the absolute expiry time.
|
||||
func (m *Meta) GetAbsoluteExpiry() int64 {
|
||||
return m.expires
|
||||
}
|
||||
|
||||
// GetRelativeExpiry returns the current relative expiry time - ie. seconds until expiry.
|
||||
func (m *Meta) GetRelativeExpiry() int64 {
|
||||
if m.deleted < 0 {
|
||||
return -m.deleted
|
||||
}
|
||||
|
||||
abs := m.expires - time.Now().Unix()
|
||||
if abs < 0 {
|
||||
return 0
|
||||
}
|
||||
return abs
|
||||
}
|
||||
|
||||
// MakeCrownJewel marks the database records as a crownjewel, meaning that it will not be sent/synced to other devices.
|
||||
func (m *Meta) MakeCrownJewel() {
|
||||
m.cronjewel = true
|
||||
|
|
|
@ -2,10 +2,18 @@ package model
|
|||
|
||||
// Model provides an interface for uniformally handling database records.
|
||||
type Model interface {
|
||||
Key() string
|
||||
SetKey(key string)
|
||||
MoveTo(key string)
|
||||
Key() string // test:config
|
||||
DatabaseName() string // test
|
||||
DatabaseKey() string // config
|
||||
|
||||
SetKey(key string) // test:config
|
||||
MoveTo(key string) // test:config
|
||||
Meta() *Meta
|
||||
SetMeta(meta *Meta)
|
||||
|
||||
Marshal(format uint8) ([]byte, error)
|
||||
MarshalRecord() ([]byte, error)
|
||||
|
||||
Lock()
|
||||
Unlock()
|
||||
}
|
||||
|
|
16
database/model/model_test.go
Normal file
16
database/model/model_test.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package model
|
||||
|
||||
import "sync"
|
||||
|
||||
type TestModel struct {
|
||||
Base
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (tm *TestModel) Lock() {
|
||||
tm.lock.Lock()
|
||||
}
|
||||
|
||||
func (tm *TestModel) Unlock() {
|
||||
tm.lock.Unlock()
|
||||
}
|
|
@ -3,59 +3,74 @@ package model
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/Safing/safing-core/formats/dsd"
|
||||
"github.com/Safing/safing-core/formats/varint"
|
||||
"github.com/Safing/portbase/container"
|
||||
"github.com/Safing/portbase/formats/dsd"
|
||||
"github.com/Safing/portbase/formats/varint"
|
||||
)
|
||||
|
||||
type Wrapper struct {
|
||||
dbName string
|
||||
dbKey string
|
||||
meta *Meta
|
||||
Base
|
||||
Format uint8
|
||||
Data []byte
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewRawWrapper(database, key string, data []byte) (*Wrapper, error) {
|
||||
version, offset, err := varint.Unpack8(data)
|
||||
if version != 1 {
|
||||
return nil, fmt.Errorf("incompatible record version: %d", version)
|
||||
}
|
||||
|
||||
metaSection, n, err := varint.GetNextBlock(data[offset:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get meta section: %s", err)
|
||||
}
|
||||
offset += n
|
||||
|
||||
newMeta := &Meta{}
|
||||
_, err = newMeta.GenCodeUnmarshal(metaSection)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal meta section: %s", err)
|
||||
}
|
||||
|
||||
format, _, err := varint.Unpack8(data[offset:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get dsd format: %s", err)
|
||||
}
|
||||
|
||||
return &Wrapper{
|
||||
Base{
|
||||
database,
|
||||
key,
|
||||
newMeta,
|
||||
},
|
||||
format,
|
||||
data[offset:],
|
||||
sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewWrapper returns a new model wrapper for the given data.
|
||||
func NewWrapper(key string, meta *Meta, data []byte) (*Wrapper, error) {
|
||||
format, _, err := varint.Unpack8(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database: could not get dsd format: %s", err)
|
||||
return nil, fmt.Errorf("could not get dsd format: %s", err)
|
||||
}
|
||||
|
||||
new := &Wrapper{
|
||||
dbKey: key,
|
||||
meta: meta,
|
||||
Format: format,
|
||||
Data: data,
|
||||
}
|
||||
dbName, dbKey := ParseKey(key)
|
||||
|
||||
return new, nil
|
||||
}
|
||||
|
||||
// Key returns the key of the database record.
|
||||
func (w *Wrapper) Key() string {
|
||||
return w.dbKey
|
||||
}
|
||||
|
||||
// SetKey sets the key on the database record, it should only be called after loading the record. Use MoveTo to save the record with another key.
|
||||
func (w *Wrapper) SetKey(key string) {
|
||||
w.dbKey = key
|
||||
}
|
||||
|
||||
// MoveTo sets a new key for the record and resets all metadata, except for the secret and crownjewel status.
|
||||
func (w *Wrapper) MoveTo(key string) {
|
||||
w.dbKey = key
|
||||
w.meta.Reset()
|
||||
}
|
||||
|
||||
// Meta returns the metadata object for this record.
|
||||
func (w *Wrapper) Meta() *Meta {
|
||||
return w.meta
|
||||
}
|
||||
|
||||
// SetMeta sets the metadata on the database record, it should only be called after loading the record. Use MoveTo to save the record with another key.
|
||||
func (w *Wrapper) SetMeta(meta *Meta) {
|
||||
w.meta = meta
|
||||
return &Wrapper{
|
||||
Base{
|
||||
dbName: dbName,
|
||||
dbKey: dbKey,
|
||||
meta: meta,
|
||||
},
|
||||
format,
|
||||
data,
|
||||
sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Marshal marshals the object, without the database key or metadata
|
||||
|
@ -65,3 +80,41 @@ func (w *Wrapper) Marshal(storageType uint8) ([]byte, error) {
|
|||
}
|
||||
return w.Data, nil
|
||||
}
|
||||
|
||||
// MarshalRecord packs the object, including metadata, into a byte array for saving in a database.
|
||||
func (w *Wrapper) MarshalRecord() ([]byte, error) {
|
||||
// Duplication necessary, as the version from Base would call Base.Marshal instead of Wrapper.Marshal
|
||||
|
||||
if w.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
}
|
||||
|
||||
// version
|
||||
c := container.New([]byte{1})
|
||||
|
||||
// meta
|
||||
metaSection, err := w.meta.GenCodeMarshal(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.AppendAsBlock(metaSection)
|
||||
|
||||
// data
|
||||
dataSection, err := w.Marshal(dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Append(dataSection)
|
||||
|
||||
return c.CompileData(), nil
|
||||
}
|
||||
|
||||
// Lock locks the record.
|
||||
func (w *Wrapper) Lock() {
|
||||
w.lock.Lock()
|
||||
}
|
||||
|
||||
// Unlock unlocks the record.
|
||||
func (w *Wrapper) Unlock() {
|
||||
w.lock.Unlock()
|
||||
}
|
||||
|
|
55
database/model/wrapper_test.go
Normal file
55
database/model/wrapper_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/Safing/portbase/formats/dsd"
|
||||
)
|
||||
|
||||
func TestWrapper(t *testing.T) {
|
||||
|
||||
// check model interface compliance
|
||||
var m Model
|
||||
w := &Wrapper{}
|
||||
m = w
|
||||
_ = m
|
||||
|
||||
// create test data
|
||||
testData := []byte(`J{"a": "b"}`)
|
||||
|
||||
// test wrapper
|
||||
wrapper, err := NewWrapper("test:a", nil, testData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wrapper.Format != dsd.JSON {
|
||||
t.Error("format mismatch")
|
||||
}
|
||||
if !bytes.Equal(testData, wrapper.Data) {
|
||||
t.Error("data mismatch")
|
||||
}
|
||||
|
||||
encoded, err := wrapper.Marshal(dsd.JSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(testData, encoded) {
|
||||
t.Error("marshal mismatch")
|
||||
}
|
||||
|
||||
wrapper.SetMeta(&Meta{})
|
||||
raw, err := wrapper.MarshalRecord()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wrapper2, err := NewRawWrapper("test", "a", raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(testData, wrapper2.Data) {
|
||||
t.Error("marshal mismatch")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package database
|
||||
|
||||
// A Factory creates a new database of it's type.
|
||||
type Factory func(name, location string) (*storage.Interface, error)
|
||||
|
||||
var (
|
||||
storages map[string]Factory
|
||||
storagesLock sync.Mutex
|
||||
)
|
||||
|
||||
// RegisterStorage registers a new storage type.
|
||||
func RegisterStorage(name string, factory Factory) error {
|
||||
storagesLock.Lock()
|
||||
defer storagesLock.Unlock()
|
||||
|
||||
_, ok := storages[name]
|
||||
if ok {
|
||||
return errors.New("factory for this type already exists")
|
||||
}
|
||||
|
||||
storages[name] = factory
|
||||
return nil
|
||||
}
|
||||
|
||||
// startDatabase starts a new database with the given name, storageType at location.
|
||||
func startDatabase(name, storageType, location string) (*storage.Interface, error) {
|
||||
storagesLock.Lock()
|
||||
defer storagesLock.Unlock()
|
||||
|
||||
factory, ok := storages[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("storage of this type (%s) does not exist", storageType)
|
||||
}
|
||||
|
||||
return factory(name, location)
|
||||
}
|
Loading…
Add table
Reference in a new issue