scratch/scribble.go

228 lines
4.9 KiB
Go
Raw Normal View History

2015-11-13 21:21:11 +00:00
// scribble is a tiny JSON database
2014-12-01 22:55:19 +00:00
package scribble
import (
2014-12-16 16:59:23 +00:00
"encoding/json"
2014-12-01 22:55:19 +00:00
"fmt"
2015-11-13 18:54:26 +00:00
"github.com/jcelliott/lumber"
2014-12-01 22:55:19 +00:00
"io/ioutil"
"os"
"path/filepath"
2014-12-01 22:55:19 +00:00
"strings"
"sync"
2014-12-01 23:12:45 +00:00
)
2015-11-13 18:54:26 +00:00
const Version = "1.0.2"
2014-12-01 22:55:19 +00:00
type (
2015-11-13 18:54:26 +00:00
//
Logger interface {
Fatal(string, ...interface{})
Error(string, ...interface{})
Warn(string, ...interface{})
Info(string, ...interface{})
Debug(string, ...interface{})
Trace(string, ...interface{})
}
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 {
mutex sync.Mutex
mutexes map[string]sync.Mutex
2015-11-13 18:54:26 +00:00
dir string // the directory where scribble will create the database
log Logger // the logger scribble will log to
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
2015-11-13 18:54:26 +00:00
func New(dir string, logger Logger) (driver *Driver, err error) {
//
dir = filepath.Clean(dir)
2014-12-01 22:55:19 +00:00
//
2015-06-10 22:39:33 +00:00
if logger == nil {
2015-11-13 18:54:26 +00:00
logger = lumber.NewConsoleLogger(lumber.INFO)
2015-06-10 22:39:33 +00:00
}
2015-11-13 18:54:26 +00:00
logger.Info("Creating scribble database at '%v'...\n", dir)
//
2015-11-13 18:54:26 +00:00
driver = &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
2015-11-13 18:54:26 +00:00
return driver, mkDir(dir)
2014-12-01 22:55:19 +00:00
}
// Read a record from the database
func (d *Driver) Read(collection, resource string, v interface{}) error {
2014-12-01 22:55:19 +00:00
//
path := filepath.Join(collection, resource)
dir := filepath.Join(d.dir, path)
//
switch fi, err := stat(dir); {
2014-12-01 22:55:19 +00: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 22:55:19 +00:00
// if the path is a directory, attempt to read all entries into v
case fi.Mode().IsDir():
2014-12-01 22:55:19 +00: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)
2014-12-01 22:55:19 +00:00
// the files read from the database
var f []string
2014-12-01 22:55:19 +00: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 err
}
2014-12-01 22:55:19 +00: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 22:55:19 +00:00
// if the path is a file, attempt to read the single file
case fi.Mode().IsRegular():
2015-10-12 19:44:49 +00:00
// read record from database
b, err := ioutil.ReadFile(dir + ".json")
2014-12-01 22:55:19 +00:00
if err != nil {
return err
2014-12-01 22:55:19 +00:00
}
// unmarshal data
return json.Unmarshal(b, &v)
2014-12-01 22:55:19 +00:00
}
return nil
2014-12-01 22:55:19 +00: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 {
mutex := d.getOrCreateMutex(collection)
mutex.Lock()
defer mutex.Unlock()
//
dir := filepath.Join(d.dir, collection)
//
b, err := json.MarshalIndent(v, "", "\t")
if err != nil {
return err
}
// create collection directory
if err := mkDir(dir); err != nil {
return err
}
finalPath := filepath.Join(dir, resource+".json")
tmpPath := finalPath + "~"
// write marshaled data to the temp file
if err := ioutil.WriteFile(tmpPath, b, 0644); err != nil {
return err
}
// move final file into place
return os.Rename(tmpPath, finalPath)
}
// Delete locks that database and then attempts to remove the collection/resource
// specified by [path]
func (d *Driver) Delete(collection, resource string) error {
path := filepath.Join(collection, resource)
//
mutex := d.getOrCreateMutex(path)
mutex.Lock()
defer mutex.Unlock()
2014-12-01 22:55:19 +00:00
//
dir := filepath.Join(d.dir, path)
2014-12-01 22:55:19 +00:00
switch fi, err := stat(dir); {
// 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 22:55:19 +00:00
// remove directory and all contents
case fi.Mode().IsDir():
return os.RemoveAll(dir)
// remove file
case fi.Mode().IsRegular():
return os.RemoveAll(dir + ".json")
}
return nil
}
//
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")
}
return
}
2014-12-01 22:55:19 +00:00
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
d.mutex.Lock()
defer d.mutex.Unlock()
m, 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 {
m = sync.Mutex{}
d.mutexes[collection] = m
2014-12-01 22:55:19 +00:00
}
return m
2014-12-01 22:55:19 +00:00
}
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
}