2015-06-02 10:46:25 -04:00
|
|
|
// Copyright (c) 2015 Pagoda Box Inc
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v.
|
|
|
|
// 2.0. If a copy of the MPL was not distributed with this file, You can obtain one
|
|
|
|
// at http://mozilla.org/MPL/2.0/.
|
|
|
|
//
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// scribble is a tiny JSON flat file store
|
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"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"strings"
|
2014-12-16 16:30:28 -05:00
|
|
|
"sync"
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-09-29 11:12:55 -04:00
|
|
|
"github.com/nanobox-io/golang-hatchet"
|
2014-12-01 18:12:45 -05:00
|
|
|
)
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
const Version = "0.5.0"
|
2014-12-01 17:55:19 -05:00
|
|
|
|
|
|
|
type (
|
|
|
|
|
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 {
|
2014-12-16 16:30:28 -05:00
|
|
|
mutexes map[string]sync.Mutex
|
2015-07-07 19:37:08 -04:00
|
|
|
dir string // the directory where scribble will create the database
|
2015-10-12 15:30:50 -04:00
|
|
|
log hatchet.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
|
2014-12-16 11:59:23 -05:00
|
|
|
func New(dir string, logger hatchet.Logger) (*Driver, error) {
|
|
|
|
fmt.Printf("Creating database directory at '%v'...\n", dir)
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2014-12-16 16:30:28 -05:00
|
|
|
//
|
2015-06-10 18:39:33 -04:00
|
|
|
if logger == nil {
|
|
|
|
logger = hatchet.DevNullLogger{}
|
|
|
|
}
|
2014-12-16 16:30:28 -05:00
|
|
|
|
|
|
|
//
|
|
|
|
scribble := &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-07-07 19:37:08 -04:00
|
|
|
// create database
|
2014-12-16 11:59:23 -05:00
|
|
|
if err := mkDir(scribble.dir); err != nil {
|
|
|
|
return nil, err
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
2014-12-16 11:59:23 -05:00
|
|
|
return scribble, nil
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// Write locks the database and attempts to write the record to the database under
|
|
|
|
// the [collection] specified with the [resource] name given
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// // write [fish] to database
|
|
|
|
// Write("/fish/onefish", fish{name:"onefish"})
|
|
|
|
func (d *Driver) Write(collection, resource string, v interface{}) error {
|
2014-12-16 16:30:28 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
mutex := d.getOrCreateMutex(collection)
|
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 15:30:50 -04:00
|
|
|
dir := d.dir + "/" + collection
|
2014-12-01 17:55:19 -05:00
|
|
|
|
|
|
|
//
|
2015-10-12 15:30:50 -04:00
|
|
|
b, err := json.MarshalIndent(v, "", "\t")
|
2014-12-16 16:30:28 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-07-07 19:37:08 -04:00
|
|
|
// create collection directory
|
2014-12-16 16:30:28 -05:00
|
|
|
if err := mkDir(dir); err != nil {
|
|
|
|
return err
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
finalPath := dir + "/" + resource + ".json"
|
2015-10-07 13:24:38 -04:00
|
|
|
tmpPath := finalPath + "~"
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// write marshaled data to the temp file
|
2015-10-12 14:05:06 -04:00
|
|
|
if err := ioutil.WriteFile(tmpPath, b, 0644); err != nil {
|
2014-12-16 16:30:28 -05:00
|
|
|
return err
|
2014-12-16 11:59:23 -05:00
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-07 13:24:38 -04:00
|
|
|
// move final file into place
|
2015-10-07 13:36:38 -04:00
|
|
|
return os.Rename(tmpPath, finalPath)
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// Read a record from the database
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// // read a single fish
|
|
|
|
// Read("/fish/twofish", &fish)
|
|
|
|
//
|
|
|
|
// // read all fish
|
|
|
|
// Read("/fish", &fish)
|
|
|
|
func (d *Driver) Read(path string, v interface{}) error {
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
dir := d.dir + "/" + path
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
//
|
|
|
|
fi, err := os.Stat(path)
|
2014-12-01 17:55:19 -05:00
|
|
|
if err != nil {
|
2014-12-16 16:30:28 -05:00
|
|
|
return err
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
switch {
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// if the path is a directory, attempt to read all entries into v
|
|
|
|
case fi.Mode().IsDir():
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// read all the files in the transaction.Collection
|
|
|
|
files, err := ioutil.ReadDir(dir)
|
|
|
|
if err != nil {
|
|
|
|
// an error here just means the collection is either empty or doesn't exist
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// the files read from the database
|
|
|
|
var f []string
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04: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(dir + "/" + file.Name())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// append read file
|
|
|
|
f = append(f, string(b))
|
|
|
|
}
|
|
|
|
|
|
|
|
// unmarhsal the read files as a comma delimeted byte array
|
|
|
|
return json.Unmarshal([]byte("["+strings.Join(f, ",")+"]"), v)
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// if the path is a file, attempt to read the single file
|
|
|
|
case !fi.Mode().IsDir():
|
|
|
|
// read record from database
|
|
|
|
b, err := ioutil.ReadFile(dir + ".json")
|
2014-12-01 17:55:19 -05:00
|
|
|
if err != nil {
|
2014-12-16 16:30:28 -05:00
|
|
|
return err
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// unmarshal data into the transaction.Container
|
|
|
|
return json.Unmarshal(b, &v)
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
return nil
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// Delete locks that database and then attempts to remove the collection/resource
|
|
|
|
// specified by [path]
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// // delete the fish 'redfish.json'
|
|
|
|
// Delete("/fish/redfish")
|
|
|
|
//
|
|
|
|
// // delete the fish collection
|
|
|
|
// Delete("/fish")
|
|
|
|
func (d *Driver) Delete(path string) error {
|
2014-12-16 16:30:28 -05: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 15:30:50 -04:00
|
|
|
// stat the file to determine if it is a file or dir
|
|
|
|
fi, err := os.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
switch {
|
|
|
|
// remove the collection from database
|
|
|
|
case fi.Mode().IsDir():
|
|
|
|
return os.Remove(d.dir + "/" + path)
|
2014-12-01 17:55:19 -05:00
|
|
|
|
2015-10-12 15:30:50 -04:00
|
|
|
// remove the record from database
|
|
|
|
default:
|
|
|
|
return os.Remove(d.dir + "/" + path + ".json")
|
|
|
|
}
|
|
|
|
}
|
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
|
|
|
|
2014-12-16 16:30:28 -05:00
|
|
|
c, 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-07 13:36:38 -04:00
|
|
|
c = sync.Mutex{}
|
|
|
|
d.mutexes[collection] = c
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2015-07-07 19:37:08 -04:00
|
|
|
// mkDir is a simple wrapper that attempts to make a directory at a specified
|
|
|
|
// location
|
2015-10-07 13:36:38 -04:00
|
|
|
func mkDir(d string) (err error) {
|
2014-12-01 17:55:19 -05:00
|
|
|
|
|
|
|
//
|
|
|
|
dir, _ := os.Stat(d)
|
|
|
|
|
2015-10-07 13:36:38 -04:00
|
|
|
switch {
|
|
|
|
case dir == nil:
|
|
|
|
err = os.MkdirAll(d, 0755)
|
|
|
|
case !dir.IsDir():
|
|
|
|
err = os.ErrInvalid
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|
|
|
|
|
2015-10-07 13:36:38 -04:00
|
|
|
return
|
2014-12-01 17:55:19 -05:00
|
|
|
}
|