621 lines
16 KiB
Go
621 lines
16 KiB
Go
/*
|
|
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
|
**
|
|
** Redistribution and use in source and binary forms, with or without
|
|
** modification, are permitted provided that the following conditions
|
|
** are met:
|
|
** 1. Redistributions of source code must retain the above copyright
|
|
** notice, this list of conditions and the following disclaimer.
|
|
** 2. Redistributions in binary form must reproduce the above copyright
|
|
** notice, this list of conditions and the following disclaimer in the
|
|
** documentation and/or other materials provided with the distribution.
|
|
**
|
|
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
|
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
** SUCH DAMAGE.
|
|
*/
|
|
|
|
package dropbox
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
// List represents a value of type list.
|
|
type List struct {
|
|
record *Record
|
|
field string
|
|
values []interface{}
|
|
}
|
|
|
|
// Fields represents a record.
|
|
type Fields map[string]value
|
|
|
|
// Record represents an entry in a table.
|
|
type Record struct {
|
|
table *Table
|
|
recordID string
|
|
fields Fields
|
|
isDeleted bool
|
|
}
|
|
|
|
// Table represents a list of records.
|
|
type Table struct {
|
|
datastore *Datastore
|
|
tableID string
|
|
records map[string]*Record
|
|
}
|
|
|
|
// DatastoreInfo represents the information about a datastore.
|
|
type DatastoreInfo struct {
|
|
ID string
|
|
handle string
|
|
revision int
|
|
title string
|
|
mtime time.Time
|
|
}
|
|
|
|
type datastoreDelta struct {
|
|
Revision int `json:"rev"`
|
|
Changes listOfChanges `json:"changes"`
|
|
Nonce *string `json:"nonce"`
|
|
}
|
|
|
|
type listOfDelta []datastoreDelta
|
|
|
|
// Datastore represents a datastore.
|
|
type Datastore struct {
|
|
manager *DatastoreManager
|
|
info DatastoreInfo
|
|
changes listOfChanges
|
|
tables map[string]*Table
|
|
isDeleted bool
|
|
autoCommit bool
|
|
changesQueue chan changeWork
|
|
}
|
|
|
|
// DatastoreManager represents all datastores linked to the current account.
|
|
type DatastoreManager struct {
|
|
dropbox *Dropbox
|
|
datastores []*Datastore
|
|
token string
|
|
}
|
|
|
|
const (
|
|
defaultDatastoreID = "default"
|
|
maxGlobalIDLength = 63
|
|
maxIDLength = 64
|
|
|
|
localIDPattern = `[a-z0-9_-]([a-z0-9._-]{0,62}[a-z0-9_-])?`
|
|
globalIDPattern = `.[A-Za-z0-9_-]{1,63}`
|
|
fieldsIDPattern = `[A-Za-z0-9._+/=-]{1,64}`
|
|
fieldsSpecialIDPattern = `:[A-Za-z0-9._+/=-]{1,63}`
|
|
)
|
|
|
|
var (
|
|
localIDRegexp *regexp.Regexp
|
|
globalIDRegexp *regexp.Regexp
|
|
fieldsIDRegexp *regexp.Regexp
|
|
fieldsSpecialIDRegexp *regexp.Regexp
|
|
)
|
|
|
|
func init() {
|
|
var err error
|
|
if localIDRegexp, err = regexp.Compile(localIDPattern); err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
if globalIDRegexp, err = regexp.Compile(globalIDPattern); err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
if fieldsIDRegexp, err = regexp.Compile(fieldsIDPattern); err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
if fieldsSpecialIDRegexp, err = regexp.Compile(fieldsSpecialIDPattern); err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
}
|
|
|
|
func isValidDatastoreID(ID string) bool {
|
|
if ID[0] == '.' {
|
|
return globalIDRegexp.MatchString(ID)
|
|
}
|
|
return localIDRegexp.MatchString(ID)
|
|
}
|
|
|
|
func isValidID(ID string) bool {
|
|
if ID[0] == ':' {
|
|
return fieldsSpecialIDRegexp.MatchString(ID)
|
|
}
|
|
return fieldsIDRegexp.MatchString(ID)
|
|
}
|
|
|
|
const (
|
|
// TypeBoolean is the returned type when the value is a bool
|
|
TypeBoolean AtomType = iota
|
|
// TypeInteger is the returned type when the value is an int
|
|
TypeInteger
|
|
// TypeDouble is the returned type when the value is a float
|
|
TypeDouble
|
|
// TypeString is the returned type when the value is a string
|
|
TypeString
|
|
// TypeBytes is the returned type when the value is a []byte
|
|
TypeBytes
|
|
// TypeDate is the returned type when the value is a Date
|
|
TypeDate
|
|
// TypeList is the returned type when the value is a List
|
|
TypeList
|
|
)
|
|
|
|
// AtomType represents the type of the value.
|
|
type AtomType int
|
|
|
|
// NewDatastoreManager returns a new DatastoreManager linked to the current account.
|
|
func (db *Dropbox) NewDatastoreManager() *DatastoreManager {
|
|
return &DatastoreManager{
|
|
dropbox: db,
|
|
}
|
|
}
|
|
|
|
// OpenDatastore opens or creates a datastore.
|
|
func (dmgr *DatastoreManager) OpenDatastore(dsID string) (*Datastore, error) {
|
|
rev, handle, _, err := dmgr.dropbox.openOrCreateDatastore(dsID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rv := &Datastore{
|
|
manager: dmgr,
|
|
info: DatastoreInfo{
|
|
ID: dsID,
|
|
handle: handle,
|
|
revision: rev,
|
|
},
|
|
tables: make(map[string]*Table),
|
|
changesQueue: make(chan changeWork),
|
|
}
|
|
if rev > 0 {
|
|
err = rv.LoadSnapshot()
|
|
}
|
|
go rv.doHandleChange()
|
|
return rv, err
|
|
}
|
|
|
|
// OpenDefaultDatastore opens the default datastore.
|
|
func (dmgr *DatastoreManager) OpenDefaultDatastore() (*Datastore, error) {
|
|
return dmgr.OpenDatastore(defaultDatastoreID)
|
|
}
|
|
|
|
// ListDatastores lists all datastores.
|
|
func (dmgr *DatastoreManager) ListDatastores() ([]DatastoreInfo, error) {
|
|
info, _, err := dmgr.dropbox.listDatastores()
|
|
return info, err
|
|
}
|
|
|
|
// DeleteDatastore deletes a datastore.
|
|
func (dmgr *DatastoreManager) DeleteDatastore(dsID string) error {
|
|
_, err := dmgr.dropbox.deleteDatastore(dsID)
|
|
return err
|
|
}
|
|
|
|
// CreateDatastore creates a global datastore with a unique ID, empty string for a random key.
|
|
func (dmgr *DatastoreManager) CreateDatastore(dsID string) (*Datastore, error) {
|
|
rev, handle, _, err := dmgr.dropbox.createDatastore(dsID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Datastore{
|
|
manager: dmgr,
|
|
info: DatastoreInfo{
|
|
ID: dsID,
|
|
handle: handle,
|
|
revision: rev,
|
|
},
|
|
tables: make(map[string]*Table),
|
|
changesQueue: make(chan changeWork),
|
|
}, nil
|
|
}
|
|
|
|
// AwaitDeltas awaits for deltas and applies them.
|
|
func (ds *Datastore) AwaitDeltas() error {
|
|
if len(ds.changes) != 0 {
|
|
return fmt.Errorf("changes already pending")
|
|
}
|
|
_, _, deltas, err := ds.manager.dropbox.await([]*Datastore{ds}, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changes, ok := deltas[ds.info.handle]
|
|
if !ok || len(changes) == 0 {
|
|
return nil
|
|
}
|
|
return ds.applyDelta(changes)
|
|
}
|
|
|
|
func (ds *Datastore) applyDelta(dds []datastoreDelta) error {
|
|
if len(ds.changes) != 0 {
|
|
return fmt.Errorf("changes already pending")
|
|
}
|
|
for _, d := range dds {
|
|
if d.Revision < ds.info.revision {
|
|
continue
|
|
}
|
|
for _, c := range d.Changes {
|
|
ds.applyChange(c)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close closes the datastore.
|
|
func (ds *Datastore) Close() {
|
|
close(ds.changesQueue)
|
|
}
|
|
|
|
// Delete deletes the datastore.
|
|
func (ds *Datastore) Delete() error {
|
|
return ds.manager.DeleteDatastore(ds.info.ID)
|
|
}
|
|
|
|
// SetTitle sets the datastore title to the given string.
|
|
func (ds *Datastore) SetTitle(t string) error {
|
|
if len(ds.info.title) == 0 {
|
|
return ds.insertRecord(":info", "info", Fields{
|
|
"title": value{
|
|
values: []interface{}{t},
|
|
},
|
|
})
|
|
}
|
|
return ds.updateField(":info", "info", "title", t)
|
|
}
|
|
|
|
// SetMTime sets the datastore mtime to the given time.
|
|
func (ds *Datastore) SetMTime(t time.Time) error {
|
|
if time.Time(ds.info.mtime).IsZero() {
|
|
return ds.insertRecord(":info", "info", Fields{
|
|
"mtime": value{
|
|
values: []interface{}{t},
|
|
},
|
|
})
|
|
}
|
|
return ds.updateField(":info", "info", "mtime", t)
|
|
}
|
|
|
|
// Rollback reverts all local changes and discards them.
|
|
func (ds *Datastore) Rollback() error {
|
|
if len(ds.changes) == 0 {
|
|
return nil
|
|
}
|
|
for i := len(ds.changes) - 1; i >= 0; i-- {
|
|
ds.applyChange(ds.changes[i].Revert)
|
|
}
|
|
ds.changes = ds.changes[:0]
|
|
return nil
|
|
}
|
|
|
|
// GetTable returns the requested table.
|
|
func (ds *Datastore) GetTable(tableID string) (*Table, error) {
|
|
if !isValidID(tableID) {
|
|
return nil, fmt.Errorf("invalid table ID %s", tableID)
|
|
}
|
|
t, ok := ds.tables[tableID]
|
|
if ok {
|
|
return t, nil
|
|
}
|
|
t = &Table{
|
|
datastore: ds,
|
|
tableID: tableID,
|
|
records: make(map[string]*Record),
|
|
}
|
|
ds.tables[tableID] = t
|
|
return t, nil
|
|
}
|
|
|
|
// Commit commits the changes registered by sending them to the server.
|
|
func (ds *Datastore) Commit() error {
|
|
rev, err := ds.manager.dropbox.putDelta(ds.info.handle, ds.info.revision, ds.changes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ds.changes = ds.changes[:0]
|
|
ds.info.revision = rev
|
|
return nil
|
|
}
|
|
|
|
// LoadSnapshot updates the state of the datastore from the server.
|
|
func (ds *Datastore) LoadSnapshot() error {
|
|
if len(ds.changes) != 0 {
|
|
return fmt.Errorf("could not load snapshot when there are pending changes")
|
|
}
|
|
rows, rev, err := ds.manager.dropbox.getSnapshot(ds.info.handle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ds.tables = make(map[string]*Table)
|
|
for _, r := range rows {
|
|
if _, ok := ds.tables[r.TID]; !ok {
|
|
ds.tables[r.TID] = &Table{
|
|
datastore: ds,
|
|
tableID: r.TID,
|
|
records: make(map[string]*Record),
|
|
}
|
|
}
|
|
ds.tables[r.TID].records[r.RowID] = &Record{
|
|
table: ds.tables[r.TID],
|
|
recordID: r.RowID,
|
|
fields: r.Data,
|
|
}
|
|
}
|
|
ds.info.revision = rev
|
|
return nil
|
|
}
|
|
|
|
// GetDatastore returns the datastore associated with this table.
|
|
func (t *Table) GetDatastore() *Datastore {
|
|
return t.datastore
|
|
}
|
|
|
|
// GetID returns the ID of this table.
|
|
func (t *Table) GetID() string {
|
|
return t.tableID
|
|
}
|
|
|
|
// Get returns the record with this ID.
|
|
func (t *Table) Get(recordID string) (*Record, error) {
|
|
if !isValidID(recordID) {
|
|
return nil, fmt.Errorf("invalid record ID %s", recordID)
|
|
}
|
|
return t.records[recordID], nil
|
|
}
|
|
|
|
// GetOrInsert gets the requested record.
|
|
func (t *Table) GetOrInsert(recordID string) (*Record, error) {
|
|
if !isValidID(recordID) {
|
|
return nil, fmt.Errorf("invalid record ID %s", recordID)
|
|
}
|
|
return t.GetOrInsertWithFields(recordID, nil)
|
|
}
|
|
|
|
// GetOrInsertWithFields gets the requested table.
|
|
func (t *Table) GetOrInsertWithFields(recordID string, fields Fields) (*Record, error) {
|
|
if !isValidID(recordID) {
|
|
return nil, fmt.Errorf("invalid record ID %s", recordID)
|
|
}
|
|
if r, ok := t.records[recordID]; ok {
|
|
return r, nil
|
|
}
|
|
if fields == nil {
|
|
fields = make(Fields)
|
|
}
|
|
if err := t.datastore.insertRecord(t.tableID, recordID, fields); err != nil {
|
|
return nil, err
|
|
}
|
|
return t.records[recordID], nil
|
|
}
|
|
|
|
// Query returns a list of records matching all the given fields.
|
|
func (t *Table) Query(fields Fields) ([]*Record, error) {
|
|
var records []*Record
|
|
|
|
next:
|
|
for _, record := range t.records {
|
|
for qf, qv := range fields {
|
|
if rv, ok := record.fields[qf]; !ok || !reflect.DeepEqual(qv, rv) {
|
|
continue next
|
|
}
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
// GetTable returns the table associated with this record.
|
|
func (r *Record) GetTable() *Table {
|
|
return r.table
|
|
}
|
|
|
|
// GetID returns the ID of this record.
|
|
func (r *Record) GetID() string {
|
|
return r.recordID
|
|
}
|
|
|
|
// IsDeleted returns whether this record was deleted.
|
|
func (r *Record) IsDeleted() bool {
|
|
return r.isDeleted
|
|
}
|
|
|
|
// DeleteRecord deletes this record.
|
|
func (r *Record) DeleteRecord() {
|
|
r.table.datastore.deleteRecord(r.table.tableID, r.recordID)
|
|
}
|
|
|
|
// HasField returns whether this field exists.
|
|
func (r *Record) HasField(field string) (bool, error) {
|
|
if !isValidID(field) {
|
|
return false, fmt.Errorf("invalid field %s", field)
|
|
}
|
|
_, ok := r.fields[field]
|
|
return ok, nil
|
|
}
|
|
|
|
// Get gets the current value of this field.
|
|
func (r *Record) Get(field string) (interface{}, bool, error) {
|
|
if !isValidID(field) {
|
|
return nil, false, fmt.Errorf("invalid field %s", field)
|
|
}
|
|
v, ok := r.fields[field]
|
|
if !ok {
|
|
return nil, false, nil
|
|
}
|
|
if v.isList {
|
|
return &List{
|
|
record: r,
|
|
field: field,
|
|
values: v.values,
|
|
}, true, nil
|
|
}
|
|
return v.values[0], true, nil
|
|
}
|
|
|
|
// GetOrCreateList gets the current value of this field.
|
|
func (r *Record) GetOrCreateList(field string) (*List, error) {
|
|
if !isValidID(field) {
|
|
return nil, fmt.Errorf("invalid field %s", field)
|
|
}
|
|
v, ok := r.fields[field]
|
|
if ok && !v.isList {
|
|
return nil, fmt.Errorf("not a list")
|
|
}
|
|
if !ok {
|
|
if err := r.table.datastore.listCreate(r.table.tableID, r.recordID, field); err != nil {
|
|
return nil, err
|
|
}
|
|
v = r.fields[field]
|
|
}
|
|
return &List{
|
|
record: r,
|
|
field: field,
|
|
values: v.values,
|
|
}, nil
|
|
}
|
|
|
|
func getType(i interface{}) (AtomType, error) {
|
|
switch i.(type) {
|
|
case bool:
|
|
return TypeBoolean, nil
|
|
case int, int32, int64:
|
|
return TypeInteger, nil
|
|
case float32, float64:
|
|
return TypeDouble, nil
|
|
case string:
|
|
return TypeString, nil
|
|
case []byte:
|
|
return TypeBytes, nil
|
|
case time.Time:
|
|
return TypeDate, nil
|
|
}
|
|
return 0, fmt.Errorf("type %s not supported", reflect.TypeOf(i).Name())
|
|
}
|
|
|
|
// GetFieldType returns the type of the given field.
|
|
func (r *Record) GetFieldType(field string) (AtomType, error) {
|
|
if !isValidID(field) {
|
|
return 0, fmt.Errorf("invalid field %s", field)
|
|
}
|
|
v, ok := r.fields[field]
|
|
if !ok {
|
|
return 0, fmt.Errorf("no such field: %s", field)
|
|
}
|
|
if v.isList {
|
|
return TypeList, nil
|
|
}
|
|
return getType(v.values[0])
|
|
}
|
|
|
|
// Set sets the value of a field.
|
|
func (r *Record) Set(field string, value interface{}) error {
|
|
if !isValidID(field) {
|
|
return fmt.Errorf("invalid field %s", field)
|
|
}
|
|
return r.table.datastore.updateField(r.table.tableID, r.recordID, field, value)
|
|
}
|
|
|
|
// DeleteField deletes the given field from this record.
|
|
func (r *Record) DeleteField(field string) error {
|
|
if !isValidID(field) {
|
|
return fmt.Errorf("invalid field %s", field)
|
|
}
|
|
return r.table.datastore.deleteField(r.table.tableID, r.recordID, field)
|
|
}
|
|
|
|
// FieldNames returns a list of fields names.
|
|
func (r *Record) FieldNames() []string {
|
|
var rv []string
|
|
|
|
rv = make([]string, 0, len(r.fields))
|
|
for k := range r.fields {
|
|
rv = append(rv, k)
|
|
}
|
|
return rv
|
|
}
|
|
|
|
// IsEmpty returns whether the list contains an element.
|
|
func (l *List) IsEmpty() bool {
|
|
return len(l.values) == 0
|
|
}
|
|
|
|
// Size returns the number of elements in the list.
|
|
func (l *List) Size() int {
|
|
return len(l.values)
|
|
}
|
|
|
|
// GetType gets the type of the n-th element in the list.
|
|
func (l *List) GetType(n int) (AtomType, error) {
|
|
if n >= len(l.values) {
|
|
return 0, fmt.Errorf("out of bound index")
|
|
}
|
|
return getType(l.values[n])
|
|
}
|
|
|
|
// Get gets the n-th element in the list.
|
|
func (l *List) Get(n int) (interface{}, error) {
|
|
if n >= len(l.values) {
|
|
return 0, fmt.Errorf("out of bound index")
|
|
}
|
|
return l.values[n], nil
|
|
}
|
|
|
|
// AddAtPos inserts the item at the n-th position in the list.
|
|
func (l *List) AddAtPos(n int, i interface{}) error {
|
|
if n > len(l.values) {
|
|
return fmt.Errorf("out of bound index")
|
|
}
|
|
err := l.record.table.datastore.listInsert(l.record.table.tableID, l.record.recordID, l.field, n, i)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l.values = l.record.fields[l.field].values
|
|
return nil
|
|
}
|
|
|
|
// Add adds the item at the end of the list.
|
|
func (l *List) Add(i interface{}) error {
|
|
return l.AddAtPos(len(l.values), i)
|
|
}
|
|
|
|
// Set sets the value of the n-th element of the list.
|
|
func (l *List) Set(n int, i interface{}) error {
|
|
if n >= len(l.values) {
|
|
return fmt.Errorf("out of bound index")
|
|
}
|
|
return l.record.table.datastore.listPut(l.record.table.tableID, l.record.recordID, l.field, n, i)
|
|
}
|
|
|
|
// Remove removes the n-th element of the list.
|
|
func (l *List) Remove(n int) error {
|
|
if n >= len(l.values) {
|
|
return fmt.Errorf("out of bound index")
|
|
}
|
|
err := l.record.table.datastore.listDelete(l.record.table.tableID, l.record.recordID, l.field, n)
|
|
l.values = l.record.fields[l.field].values
|
|
return err
|
|
}
|
|
|
|
// Move moves the element from the from-th position to the to-th.
|
|
func (l *List) Move(from, to int) error {
|
|
if from >= len(l.values) || to >= len(l.values) {
|
|
return fmt.Errorf("out of bound index")
|
|
}
|
|
return l.record.table.datastore.listMove(l.record.table.tableID, l.record.recordID, l.field, from, to)
|
|
}
|