scratch/scribble.go

226 lines
5.7 KiB
Go
Raw Normal View History

2015-06-02 14:46:25 +00: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-07-07 23:37:08 +00:00
// scribble is a tiny JSON flat file store. It uses transactions that tell it what
// actions to perform, where it is to store data, and what it is going to write
// that data from, or read the data into. It creates a very simple database
// structure under a specified directory
2014-12-01 22:55:19 +00:00
package scribble
import (
2014-12-16 16:59:23 +00:00
"encoding/json"
"errors"
2014-12-01 22:55:19 +00:00
"fmt"
"io/ioutil"
"os"
"strings"
"sync"
2014-12-01 22:55:19 +00:00
2015-09-29 15:12:55 +00:00
"github.com/nanobox-io/golang-hatchet"
2014-12-01 23:12:45 +00:00
)
2015-07-07 23:37:08 +00:00
const Version = "0.1.0"
2014-12-01 22:55:19 +00:00
type (
2015-07-07 23:37:08 +00:00
// a Driver is what is used to interact with the scribble database. It runs
// transactions, and provides log output
2014-12-01 22:55:19 +00:00
Driver struct {
mutexes map[string]sync.Mutex
2015-07-07 23:37:08 +00:00
dir string // the directory where scribble will create the database
log hatchet.Logger // the logger scirbble will log to
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// a Transactions is what is used by a Driver to complete database operations
2014-12-01 22:55:19 +00:00
Transaction struct {
2015-07-07 23:37:08 +00:00
Action string // the action for scribble to preform
Collection string // the forlder for scribble to read/write to/from
ResourceID string // the unique ID of the record
Container interface{} // what scribble will marshal from or unmarshal into
2014-12-01 22:55:19 +00:00
}
)
2015-07-07 23:37:08 +00: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 16:59:23 +00:00
func New(dir string, logger hatchet.Logger) (*Driver, error) {
fmt.Printf("Creating database directory at '%v'...\n", dir)
2014-12-01 22:55:19 +00:00
//
2015-06-10 22:39:33 +00:00
if logger == nil {
logger = hatchet.DevNullLogger{}
}
//
scribble := &Driver{
2015-06-10 22:39:33 +00:00
dir: dir,
mutexes: make(map[string]sync.Mutex),
2015-06-10 22:39:33 +00:00
log: logger,
}
2014-12-01 22:55:19 +00:00
2015-07-07 23:37:08 +00:00
// create database
2014-12-16 16:59:23 +00:00
if err := mkDir(scribble.dir); err != nil {
return nil, err
2014-12-01 22:55:19 +00:00
}
//
2014-12-16 16:59:23 +00:00
return scribble, nil
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// Transact determins the type of transactions to be run, and calls the appropriate
// method to complete the operation
2014-12-16 16:59:23 +00:00
func (d *Driver) Transact(trans Transaction) error {
2014-12-01 22:55:19 +00:00
2015-07-07 23:37:08 +00:00
// determin transaction to be run
2014-12-01 22:55:19 +00:00
switch trans.Action {
case "write":
return d.write(trans)
2014-12-01 22:55:19 +00:00
case "read":
return d.read(trans)
2014-12-01 22:55:19 +00:00
case "readall":
return d.readAll(trans)
2014-12-01 22:55:19 +00:00
case "delete":
return d.delete(trans)
2014-12-01 22:55:19 +00:00
default:
return errors.New(fmt.Sprintf("Unsupported action %+v", trans.Action))
2014-12-01 22:55:19 +00:00
}
return nil
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// write locks the database and then proceeds to write the data represented by a
// transaction.Container. It will create a direcotry that represents the collection
// to wich the record belongs (if it doesn't already exist), and write a file
// (named by he transaction.ResourceID) to that directory
func (d *Driver) write(trans Transaction) error {
mutex := d.getOrCreateMutex(trans.Collection)
mutex.Lock()
defer mutex.Unlock()
2014-12-01 22:55:19 +00:00
//
dir := d.dir + "/" + trans.Collection
//
b, err := json.MarshalIndent(trans.Container, "", "\t")
if err != nil {
return err
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// create collection directory
if err := mkDir(dir); err != nil {
return err
2014-12-01 22:55:19 +00:00
}
finalPath := dir + "/" + trans.ResourceID + ".json"
tmpPath := finalPath + "~"
2015-07-07 23:37:08 +00:00
// write marshaled data to a file, named by the resourceID
if err := ioutil.WriteFile(tmpPath, b, 0666); err != nil {
return err
2014-12-16 16:59:23 +00:00
}
2014-12-01 22:55:19 +00:00
// move final file into place
return os.Rename(tmpPath, finalPath)
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// read does the opposite operation as write. Reading a record from the database
// (named by the transaction.resourceID, found in the transaction.Collection), and
// unmarshaling the data into the transaction.Container
func (d *Driver) read(trans Transaction) error {
2014-12-01 22:55:19 +00:00
dir := d.dir + "/" + trans.Collection
2015-07-07 23:37:08 +00:00
// read record from database
b, err := ioutil.ReadFile(dir + "/" + trans.ResourceID + ".json")
2014-12-01 22:55:19 +00:00
if err != nil {
return err
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// unmarshal data into the transaction.Container
return json.Unmarshal(b, trans.Container)
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// readAll does the same operation as read, reading all the records in the specified
// transaction.Collection
func (d *Driver) readAll(trans Transaction) error {
2014-12-01 22:55:19 +00:00
dir := d.dir + "/" + trans.Collection
2015-07-07 23:37:08 +00:00
// read all the files in the transaction.Collection
2014-12-01 22:55:19 +00:00
files, err := ioutil.ReadDir(dir)
if err != nil {
2015-07-07 23:37:08 +00:00
// an error here just means an empty collection so do nothing
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// the files read from the database
2014-12-01 22:55:19 +00:00
var f []string
2015-07-07 23:37:08 +00:00
// iterate over each of the files, attempting to read the file. If successful
// append the files to the collection of read files
2014-12-01 22:55:19 +00:00
for _, file := range files {
b, err := ioutil.ReadFile(dir + "/" + file.Name())
if err != nil {
return err
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// append read file
2014-12-01 22:55:19 +00:00
f = append(f, string(b))
}
2015-07-07 23:37:08 +00:00
// unmarhsal the read files as a comma delimeted byte array
return json.Unmarshal([]byte("["+strings.Join(f, ",")+"]"), trans.Container)
2014-12-01 22:55:19 +00:00
}
2015-07-07 23:37:08 +00:00
// delete locks that database and then proceeds to remove the record (specified by
// transaction.ResourceID) from the collection
func (d *Driver) delete(trans Transaction) error {
mutex := d.getOrCreateMutex(trans.Collection)
mutex.Lock()
defer mutex.Unlock()
2014-12-01 22:55:19 +00:00
dir := d.dir + "/" + trans.Collection
2015-07-07 23:37:08 +00:00
// remove record from database
return os.Remove(dir + "/" + trans.ResourceID + ".json")
2014-12-01 22:55:19 +00:00
}
// helpers
2015-07-07 23:37:08 +00:00
// getOrCreateMutex creates a new collection specific mutex any time a collection
// is being modfied to avoid unsafe operations
func (d *Driver) getOrCreateMutex(collection string) sync.Mutex {
2014-12-01 22:55:19 +00:00
c, ok := d.mutexes[collection]
2014-12-01 22:55:19 +00:00
// if the mutex doesn't exist make it
2014-12-01 22:55:19 +00:00
if !ok {
c = sync.Mutex{}
d.mutexes[collection] = c
2014-12-01 22:55:19 +00:00
}
return c
}
2015-07-07 23:37:08 +00:00
// mkDir is a simple wrapper that attempts to make a directory at a specified
// location
func mkDir(d string) (err error) {
2014-12-01 22:55:19 +00:00
//
dir, _ := os.Stat(d)
switch {
case dir == nil:
err = os.MkdirAll(d, 0755)
case !dir.IsDir():
err = os.ErrInvalid
2014-12-01 22:55:19 +00:00
}
return
2014-12-01 22:55:19 +00:00
}