2015-11-13 16:21:11 -05:00
|
|
|
// scribble is a tiny JSON database
|
2014-12-01 17:55:19 -05:00
|
|
|
package scribble
|
|
|
|
|
|
|
|
import (
|
2014-12-16 11:59:23 -05:00
|
|
|
"encoding/json"
|
2014-12-01 17:55:19 -05:00
|
|
|
"fmt"
|
2015-11-13 13:54:26 -05:00
|
|
|
"github.com/jcelliott/lumber"
|
2014-12-01 17:55:19 -05:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2015-10-19 17:01:40 -04:00
|
|
|
"path/filepath"
|
2014-12-16 16:30:28 -05:00
|
|
|
"sync"
|
2014-12-01 18:12:45 -05:00
|
|
|
)
|
|
|
|
|
2015-11-13 17:19:09 -05:00
|
|
|
const Version = "1.0.3"
|
2014-12-01 17:55:19 -05:00
|
|
|
|
|
|
|
type (
|
|
|
|
|
2015-11-13 13:54:26 -05:00
|
|
|
//
|
|
|
|
Logger interface {
|
|
|
|
Fatal(string, ...interface{})
|
|
|
|
Error(string, ...interface{})
|
|
|
|
Warn(string, ...interface{})
|
|
|
|
Info(string, ...interface{})
|
|
|
|
Debug(string, ...interface{})
|
|
|
|
Trace(string, ...interface{})
|
|
|
|
}
|
|
|
|
|
2015-07-07 19:37:08 -04:00
|
|
|
// a Driver is what is used to interact with the scribble database. It runs
|
|
|
|
// transactions, and provides log output
|
2014-12-01 17:55:19 -05:00
|
|
|
Driver struct {
|
2015-10-20 13:06:33 -04:00
|
|
|
mutex sync.Mutex
|
2014-12-16 16:30:28 -05:00
|
|
|
mutexes map[string]sync.Mutex
|
2015-11-13 13:54:26 -05:00
|
|
|
dir string // the directory where scribble will create the database
|
|
|
|
log Logger // the logger scribble will log to
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
2015-10-12 14:14:54 -04:00
|
|
|
)
|
|
|
|
|
2015-07-07 19:37:08 -04:00
|
|
|
// New creates a new scribble database at the desired directory location, and
|
|
|
|
// returns a *Driver to then use for interacting with the database
|
2015-11-13 13:54:26 -05:00
|
|
|
func New(dir string, logger Logger) (driver *Driver, err error) {
|
2015-10-19 17:01:40 -04:00
|
|
|
|
|
|
|
//
|
|
|
|
dir = filepath.Clean(dir)
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 12:46:22 -05:00
|
|
|
// ensure the database location doesn't already exist (we don't want to overwrite
|
|
|
|
// any existing files/database)
|
|
|
|
if _, err := os.Stat(dir); err == nil {
|
|
|
|
fmt.Printf("Unable to create database, '%s' already exists. Please specify a different location.\n", dir)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2014-12-16 16:30:28 -05:00
|
|
|
//
|
2015-06-10 18:39:33 -04:00
|
|
|
if logger == nil {
|
2015-11-13 13:54:26 -05:00
|
|
|
logger = lumber.NewConsoleLogger(lumber.INFO)
|
2015-06-10 18:39:33 -04:00
|
|
|
}
|
2014-12-16 16:30:28 -05:00
|
|
|
|
|
|
|
//
|
2015-11-13 13:54:26 -05:00
|
|
|
driver = &Driver{
|
2015-06-10 18:39:33 -04:00
|
|
|
dir: dir,
|
2014-12-16 16:30:28 -05:00
|
|
|
mutexes: make(map[string]sync.Mutex),
|
2015-06-10 18:39:33 -04:00
|
|
|
log: logger,
|
2014-12-16 16:30:28 -05:00
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
logger.Info("Creating scribble database at '%v'...\n", dir)
|
|
|
|
|
2015-07-07 19:37:08 -04:00
|
|
|
// create database
|
2015-11-16 01:44:00 -05:00
|
|
|
return driver, os.MkdirAll(dir, 0755)
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// Write locks the database and attempts to write the record to the database under
|
|
|
|
// the [collection] specified with the [resource] name given
|
|
|
|
func (d *Driver) Write(collection, resource string, v interface{}) error {
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// ensure there is a place to save record
|
|
|
|
if collection == "" {
|
|
|
|
return fmt.Errorf("Missing collection - no place to save record!")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure there is a resource (name) to save record as
|
|
|
|
if resource == "" {
|
|
|
|
return fmt.Errorf("Missing resource - unable to save record (no name)!")
|
|
|
|
}
|
|
|
|
|
|
|
|
mutex := d.getOrCreateMutex(collection)
|
|
|
|
mutex.Lock()
|
|
|
|
defer mutex.Unlock()
|
2015-10-19 17:01:40 -04:00
|
|
|
|
|
|
|
//
|
2015-11-16 01:44:00 -05:00
|
|
|
dir := filepath.Join(d.dir, collection)
|
|
|
|
fnlPath := filepath.Join(dir, resource+".json")
|
|
|
|
tmpPath := fnlPath + ".tmp"
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// create collection directory
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
//
|
|
|
|
b, err := json.MarshalIndent(v, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// write marshaled data to the temp file
|
|
|
|
if err := ioutil.WriteFile(tmpPath, b, 0644); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// move final file into place
|
|
|
|
return os.Rename(tmpPath, fnlPath)
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// Read a record from the database
|
|
|
|
func (d *Driver) Read(collection, resource string, v interface{}) error {
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// ensure there is a place to save record
|
|
|
|
if collection == "" {
|
|
|
|
return fmt.Errorf("Missing collection - no place to save record!")
|
|
|
|
}
|
2015-10-12 15:30:50 -04:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// ensure there is a resource (name) to save record as
|
|
|
|
if resource == "" {
|
|
|
|
return fmt.Errorf("Missing resource - unable to save record (no name)!")
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
//
|
|
|
|
record := filepath.Join(d.dir, collection, resource)
|
2015-10-12 15:44:49 -04:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// check to see if file exists
|
|
|
|
if _, err := stat(record); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// read record from database
|
|
|
|
b, err := ioutil.ReadFile(record + ".json")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// unmarshal data
|
|
|
|
return json.Unmarshal(b, &v)
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// ReadAll records from a collection; this is returned as a slice of strings because
|
2015-11-16 12:46:22 -05:00
|
|
|
// there is no way of knowing what type the record is.
|
2015-11-16 01:44:00 -05:00
|
|
|
func (d *Driver) ReadAll(collection string) ([]string, error) {
|
2015-10-19 17:01:40 -04:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// ensure there is a collection to read
|
|
|
|
if collection == "" {
|
|
|
|
return nil, fmt.Errorf("Missing collection - unable to record location!")
|
|
|
|
}
|
2015-10-19 17:01:40 -04:00
|
|
|
|
|
|
|
//
|
|
|
|
dir := filepath.Join(d.dir, collection)
|
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// check to see if collection (directory) exists
|
|
|
|
if _, err := stat(dir); err != nil {
|
|
|
|
return nil, err
|
2015-10-19 17:01:40 -04:00
|
|
|
}
|
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// read all the files in the transaction.Collection; an error here just means
|
|
|
|
// the collection is either empty or doesn't exist
|
|
|
|
files, _ := ioutil.ReadDir(dir)
|
2015-10-19 17:01:40 -04:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// the files read from the database
|
|
|
|
var records []string
|
2015-10-19 17:01:40 -04:00
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// iterate over each of the files, attempting to read the file. If successful
|
|
|
|
// append the files to the collection of read files
|
|
|
|
for _, file := range files {
|
|
|
|
b, err := ioutil.ReadFile(filepath.Join(dir, file.Name()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// append read file
|
|
|
|
records = append(records, string(b))
|
2015-10-19 17:01:40 -04:00
|
|
|
}
|
|
|
|
|
2015-11-16 01:44:00 -05:00
|
|
|
// unmarhsal the read files as a comma delimeted byte array
|
|
|
|
return records, nil
|
2015-10-19 17:01:40 -04:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// Delete locks that database and then attempts to remove the collection/resource
|
|
|
|
// specified by [path]
|
2015-10-29 13:22:23 -04:00
|
|
|
func (d *Driver) Delete(collection, resource string) error {
|
|
|
|
path := filepath.Join(collection, resource)
|
2015-10-19 17:01:40 -04:00
|
|
|
//
|
2015-10-12 15:30:50 -04:00
|
|
|
mutex := d.getOrCreateMutex(path)
|
2014-12-16 16:30:28 -05:00
|
|
|
mutex.Lock()
|
2015-10-07 13:36:38 -04:00
|
|
|
defer mutex.Unlock()
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 19:12:29 -04:00
|
|
|
//
|
2015-10-19 17:01:40 -04:00
|
|
|
dir := filepath.Join(d.dir, path)
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-19 17:01:40 -04:00
|
|
|
switch fi, err := stat(dir); {
|
2015-10-12 19:12:29 -04:00
|
|
|
|
2015-10-19 17:01:40 -04:00
|
|
|
// if fi is nil or error is not nil return
|
|
|
|
case fi == nil, err != nil:
|
|
|
|
return fmt.Errorf("Unable to find file or directory named %v\n", path)
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-19 17:01:40 -04:00
|
|
|
// remove directory and all contents
|
|
|
|
case fi.Mode().IsDir():
|
|
|
|
return os.RemoveAll(dir)
|
2015-10-12 19:12:29 -04:00
|
|
|
|
2015-10-19 17:01:40 -04:00
|
|
|
// remove file
|
|
|
|
case fi.Mode().IsRegular():
|
|
|
|
return os.RemoveAll(dir + ".json")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2015-10-12 19:12:29 -04:00
|
|
|
|
2015-10-19 17:01:40 -04:00
|
|
|
//
|
|
|
|
func stat(path string) (fi os.FileInfo, err error) {
|
|
|
|
|
|
|
|
// check for dir, if path isn't a directory check to see if it's a file
|
|
|
|
if fi, err = os.Stat(path); os.IsNotExist(err) {
|
|
|
|
fi, err = os.Stat(path + ".json")
|
2015-10-12 15:30:50 -04:00
|
|
|
}
|
2015-10-12 19:12:29 -04:00
|
|
|
|
2015-10-19 17:01:40 -04:00
|
|
|
return
|
2015-10-12 15:30:50 -04:00
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-07-07 19:37:08 -04:00
|
|
|
// getOrCreateMutex creates a new collection specific mutex any time a collection
|
|
|
|
// is being modfied to avoid unsafe operations
|
2014-12-16 16:30:28 -05:00
|
|
|
func (d *Driver) getOrCreateMutex(collection string) sync.Mutex {
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-20 13:06:33 -04:00
|
|
|
d.mutex.Lock()
|
|
|
|
defer d.mutex.Unlock()
|
|
|
|
|
|
|
|
m, ok := d.mutexes[collection]
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2014-12-16 16:30:28 -05:00
|
|
|
// if the mutex doesn't exist make it
|
2014-12-01 17:55:19 -05:00
|
|
|
if !ok {
|
2015-10-20 13:06:33 -04:00
|
|
|
m = sync.Mutex{}
|
|
|
|
d.mutexes[collection] = m
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-20 13:06:33 -04:00
|
|
|
return m
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|