Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Package scribble is a tiny JSON database
package scribble

import (
"os"
"path/filepath"
"sync"

"github.com/jcelliott/lumber"
)

// Version is the current version of the project
const Version = "1.1"

// New creates a new scribble database at the desired directory location, and
// returns a *Driver to then use for interacting with the database
func New(dir string, options *Options) (*Driver, error) {

//
dir = filepath.Clean(dir)

// create default options
opts := Options{}

// if options are passed in, use those
if options != nil {
opts = *options
}

// if no logger is provided, create a default
if opts.Logger == nil {
opts.Logger = lumber.NewConsoleLogger(lumber.INFO)
}

//
driver := Driver{
dir: dir,
mutexes: make(map[string]*sync.Mutex),
log: opts.Logger,
}

// if the database already exists, just use it
if _, err := os.Stat(dir); err == nil {
opts.Logger.Debug("Using '%s' (database already exists)\n", dir)
return &driver, nil
}

// if the database doesn't exist create it
opts.Logger.Debug("Creating scribble database at '%s'...\n", dir)
return &driver, os.MkdirAll(dir, 0755)
}
32 changes: 32 additions & 0 deletions io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package scribble is a tiny JSON database
package scribble

import (
"os"
"sync"
)

// stat checks for dir, if path isn't a directory check to see if it's a file
func stat(path string) (fi os.FileInfo, err error) {
if fi, err = os.Stat(path); os.IsNotExist(err) {
fi, err = os.Stat(path + ".json")
}
return
}

// 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 {
// create mutex
d.mutex.Lock()
defer d.mutex.Unlock()

// if the mutex doesn't exist make it
m, ok := d.mutexes[collection]
if !ok {
m = &sync.Mutex{}
d.mutexes[collection] = m
}

return m
}
74 changes: 74 additions & 0 deletions read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Package scribble is a tiny JSON database
package scribble

import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
)

// Read a record from the database
func (d *Driver) Read(collection, resource string, v interface{}) error {
// 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)!")
}

// create full path to record and check to see if file exists
record := filepath.Join(d.dir, collection, resource)
if _, err := stat(record); err != nil {
return err
}

// read record from database
b, err := ioutil.ReadFile(record + ".json")
if err != nil {
return err
}

// unmarshal data
return json.Unmarshal(b, &v)
}

// ReadAll records from a collection; this is returned as a slice of strings because
// there is no way of knowing what type the record is.
func (d *Driver) ReadAll(collection string) ([]string, error) {
// ensure there is a collection to read
if collection == "" {
return nil, fmt.Errorf("Missing collection - unable to record location!")
}

// create full path to collection and check to see if directory exists
dir := filepath.Join(d.dir, collection)
if _, err := stat(dir); err != nil {
return nil, err
}

// 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)

// the files read from the database
var records []string

// 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))
}

// unmarhsal the read files as a comma delimeted byte array
return records, nil
}
32 changes: 32 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package scribble is a tiny JSON database
package scribble

import (
"sync"
)

type (
// Logger is a generic logger interface
Logger interface {
Fatal(string, ...interface{})
Error(string, ...interface{})
Warn(string, ...interface{})
Info(string, ...interface{})
Debug(string, ...interface{})
Trace(string, ...interface{})
}

// Driver is what is used to interact with the scribble database. It runs
// transactions, and provides log output
Driver struct {
mutex sync.Mutex
mutexes map[string]*sync.Mutex
dir string // the directory where scribble will create the database
log Logger // the logger scribble will log to
}

// Options uses for specification of working golang-scribble
Options struct {
Logger // the logger scribble will use (configurable)
}
)
83 changes: 83 additions & 0 deletions write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Package scribble is a tiny JSON database
package scribble

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)

// 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 {
// 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)!")
}

// create mutex on collection
mutex := d.getOrCreateMutex(collection)
mutex.Lock()
defer mutex.Unlock()

// create full paths to collection, final resource file, and temp file
dir := filepath.Join(d.dir, collection)
fnlPath := filepath.Join(dir, resource+".json")
tmpPath := fnlPath + ".tmp"

// create collection directory
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}

// marshal input
b, err := json.MarshalIndent(v, "", "\t")
if err != nil {
return err
}

// 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, fnlPath)
}

// Delete locks that database and then attempts to remove the collection/resource
// specified by [path]
func (d *Driver) Delete(collection, resource string) error {
// create full path to resource
path := filepath.Join(collection, resource)

// create mutex
mutex := d.getOrCreateMutex(collection)
mutex.Lock()
defer mutex.Unlock()

// create full path to directory
dir := filepath.Join(d.dir, path)
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)

// 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
}