stacktic
/
dropbox
Archived
1
0
Fork 0
This repository has been archived on 2021-02-07. You can view files and clone it, but cannot push or open issues or pull requests.
dropbox/datastores.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)
}