Forked from github.com/sdomino/scribble

This commit is contained in:
Atlas Cove 2021-09-01 23:04:46 +01:00
parent 8f0b323da1
commit 8ce8dfb12e
4 changed files with 30 additions and 170 deletions

View File

@ -1,7 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Steve Domino Copyright (c) 2019 Steve Domino, Atlas Cove
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,18 +1,18 @@
Scribble [![GoDoc](https://godoc.org/github.com/boltdb/bolt?status.svg)](http://godoc.org/github.com/sdomino/scribble) [![Go Report Card](https://goreportcard.com/badge/github.com/sdomino/scribble)](https://goreportcard.com/report/github.com/sdomino/scribble) Scratch [![GoDoc](https://godoc.org/github.com/boltdb/bolt?status.svg)](http://godoc.org/github.com/sdomino/scribble) [![Go Report Card](https://goreportcard.com/badge/github.com/sdomino/scribble)](https://goreportcard.com/report/github.com/sdomino/scribble)
-------- --------
A tiny JSON database in Golang A tiny BSON database in Golang, a drop-in replacement for [scribble](https://github.com/sdomino/scribble).
### Installation ### Installation
Install using `go get github.com/sdomino/scribble`. Install using `go get git.sdf.org/Atlas48/scratch`.
### Usage ### Usage
```go ```go
// a new scribble driver, providing the directory where it will be writing to, // a new scratch driver, providing the directory where it will be writing to,
// and a qualified logger if desired // and a qualified logger if desired
db, err := scribble.New(dir, nil) db, err := scratch.New(dir, nil)
if err != nil { if err != nil {
fmt.Println("Error", err) fmt.Println("Error", err)
} }

View File

@ -1,8 +1,7 @@
// Package scribble is a tiny JSON database // Package scratch is a tiny, flat-file BSON database
package scribble package scratch
import ( import (
"encoding/json" "gopkg.in/mgo.v2/bson"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -10,18 +9,14 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"github.com/jcelliott/lumber" "github.com/jcelliott/lumber"
) )
// Version is the current version of the project // Version is the current version of the project
const Version = "1.0.4" const Version = "1.0.4"
var ( var (
ErrMissingResource = errors.New("missing resource - unable to save record") ErrMissingResource = errors.New("missing resource - unable to save record")
ErrMissingCollection = errors.New("missing collection - no place to save record") ErrMissingCollection = errors.New("missing collection - no place to save record")
) )
type ( type (
// Logger is a generic logger interface // Logger is a generic logger interface
Logger interface { Logger interface {
@ -32,230 +27,169 @@ type (
Debug(string, ...interface{}) Debug(string, ...interface{})
Trace(string, ...interface{}) Trace(string, ...interface{})
} }
// Driver is what is used to interact with the scratch database. It runs
// Driver is what is used to interact with the scribble database. It runs
// transactions, and provides log output // transactions, and provides log output
Driver struct { Driver struct {
mutex sync.Mutex mutex sync.Mutex
mutexes map[string]*sync.Mutex mutexes map[string]*sync.Mutex
dir string // the directory where scribble will create the database dir string // the directory where scratch will create the database
log Logger // the logger scribble will log to log Logger // the logger scratch will log to
} }
) )
// Options uses for specification of working golang-scratch
// Options uses for specification of working golang-scribble
type Options struct { type Options struct {
Logger // the logger scribble will use (configurable) Logger // the logger scratch will use (configurable)
} }
// New creates a new scratch database at the desired directory location, and
// New creates a new scribble database at the desired directory location, and
// returns a *Driver to then use for interacting with the database // returns a *Driver to then use for interacting with the database
func New(dir string, options *Options) (*Driver, error) { func New(dir string, options *Options) (*Driver, error) {
// //
dir = filepath.Clean(dir) dir = filepath.Clean(dir)
// create default options // create default options
opts := Options{} opts := Options{}
// if options are passed in, use those // if options are passed in, use those
if options != nil { if options != nil { opts = *options }
opts = *options
}
// if no logger is provided, create a default // if no logger is provided, create a default
if opts.Logger == nil { if opts.Logger == nil {
opts.Logger = lumber.NewConsoleLogger(lumber.INFO) opts.Logger = lumber.NewConsoleLogger(lumber.INFO)
} }
// //
driver := Driver{ driver := Driver{
dir: dir, dir: dir,
mutexes: make(map[string]*sync.Mutex), mutexes: make(map[string]*sync.Mutex),
log: opts.Logger, log: opts.Logger,
} }
// if the database already exists, just use it // if the database already exists, just use it
if _, err := os.Stat(dir); err == nil { if _, err := os.Stat(dir); err == nil {
opts.Logger.Debug("Using '%s' (database already exists)\n", dir) opts.Logger.Debug("Using '%s' (database already exists)\n", dir)
return &driver, nil return &driver, nil
} }
// if the database doesn't exist create it // if the database doesn't exist create it
opts.Logger.Debug("Creating scribble database at '%s'...\n", dir) opts.Logger.Debug("Creating scratch database at '%s'...\n", dir)
return &driver, os.MkdirAll(dir, 0755) return &driver, os.MkdirAll(dir, 0755)
} }
// Write locks the database and attempts to write the record to the database under // Write locks the database and attempts to write the record to the database under
// the [collection] specified with the [resource] name given // the [collection] specified with the [resource] name given
func (d *Driver) Write(collection, resource string, v interface{}) error { func (d *Driver) Write(collection, resource string, v interface{}) error {
// ensure there is a place to save record // ensure there is a place to save record
if collection == "" { if collection == "" {
return ErrMissingCollection return ErrMissingCollection
} }
// ensure there is a resource (name) to save record as // ensure there is a resource (name) to save record as
if resource == "" { if resource == "" {
return ErrMissingResource return ErrMissingResource
} }
mutex := d.getOrCreateMutex(collection) mutex := d.getOrCreateMutex(collection)
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
// //
dir := filepath.Join(d.dir, collection) dir := filepath.Join(d.dir, collection)
fnlPath := filepath.Join(dir, resource+".json") fnlPath := filepath.Join(dir, resource+".bson")
tmpPath := fnlPath + ".tmp" tmpPath := fnlPath + ".tmp"
return write(dir, tmpPath, fnlPath, v) return write(dir, tmpPath, fnlPath, v)
} }
func write(dir, tmpPath, dstPath string, v interface{}) error { func write(dir, tmpPath, dstPath string, v interface{}) error {
// create collection directory // create collection directory
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return err return err
} }
// marshal the pointer to a non-struct and indent with tab // marshal the pointer to a non-struct and indent with tab
b, err := json.MarshalIndent(v, "", "\t") b, err := bson.Marshal(v, "", "\t")
if err != nil { if err != nil {
return err return err
} }
// write marshaled data to the temp file // write marshaled data to the temp file
if err := ioutil.WriteFile(tmpPath, b, 0644); err != nil { if err := ioutil.WriteFile(tmpPath, b, 0644); err != nil {
return err return err
} }
// move final file into place // move final file into place
return os.Rename(tmpPath, dstPath) return os.Rename(tmpPath, dstPath)
} }
// Read a record from the database // Read a record from the database
func (d *Driver) Read(collection, resource string, v interface{}) error { func (d *Driver) Read(collection, resource string, v interface{}) error {
// ensure there is a place to save record // ensure there is a place to save record
if collection == "" { if collection == "" {
return ErrMissingCollection return ErrMissingCollection
} }
// ensure there is a resource (name) to save record as // ensure there is a resource (name) to save record as
if resource == "" { if resource == "" {
return ErrMissingResource return ErrMissingResource
} }
// //
record := filepath.Join(d.dir, collection, resource) record := filepath.Join(d.dir, collection, resource)
// check to see if file exists // check to see if file exists
if _, err := stat(record); err != nil { if _, err := stat(record); err != nil {
return err return err
} }
// read record from database // read record from database
return read(record, v) return read(record, v)
} }
func read(record string, v interface{}) error { func read(record string, v interface{}) error {
b, err := ioutil.ReadFile(record + ".bson")
b, err := ioutil.ReadFile(record + ".json") if err != nil { return err }
if err != nil {
return err
}
// unmarshal data // unmarshal data
return json.Unmarshal(b, &v) return bson.Unmarshal(b, &v)
} }
// ReadAll records from a collection; this is returned as a slice of strings because // 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. // there is no way of knowing what type the record is.
func (d *Driver) ReadAll(collection string) ([][]byte, error) { func (d *Driver) ReadAll(collection string) ([][]byte, error) {
// ensure there is a collection to read // ensure there is a collection to read
if collection == "" { if collection == "" {
return nil, ErrMissingCollection return nil, ErrMissingCollection
} }
// //
dir := filepath.Join(d.dir, collection) dir := filepath.Join(d.dir, collection)
os.MkdirAll(dir, 0777) os.MkdirAll(dir, 0777)
// read all the files in the transaction.Collection; an error here just means // read all the files in the transaction.Collection; an error here just means
// the collection is either empty or doesn't exist // the collection is either empty or doesn't exist
files, err := ioutil.ReadDir(dir) files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil { return nil, err }
return nil, err
}
// read all the files in the transaction.Collection; an error here just means // read all the files in the transaction.Collection; an error here just means
// the collection is either empty or doesn't exist // the collection is either empty or doesn't exist
files, err := ioutil.ReadDir(dir) files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil { return nil, err }
return nil, err
}
return readAll(files, dir) return readAll(files, dir)
} }
func readAll(files []os.FileInfo, dir string) ([][]byte, error) { func readAll(files []os.FileInfo, dir string) ([][]byte, error) {
// the files read from the database // the files read from the database
var records [][]byte var records [][]byte
// iterate over each of the files, attempting to read the file. If successful // iterate over each of the files, attempting to read the file. If successful
// append the files to the collection of read // append the files to the collection of read
for _, file := range files { for _, file := range files {
b, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) b, err := ioutil.ReadFile(filepath.Join(dir, file.Name()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// append read file // append read file
records = append(records, b) records = append(records, b)
} }
// unmarhsal the read files as a comma delimeted byte array // unmarhsal the read files as a comma delimeted byte array
return records, nil return records, nil
} }
// List ID of records from a collection; this is returned as a slice of strings. // List ID of records from a collection; this is returned as a slice of strings.
func (d *Driver) List(collection string) ([]string, error) { func (d *Driver) List(collection string) ([]string, error) {
// ensure there is a collection to read // ensure there is a collection to read
if collection == "" { if collection == "" {
return nil, ErrMissingCollection return nil, ErrMissingCollection
} }
// //
dir := filepath.Join(d.dir, collection) dir := filepath.Join(d.dir, collection)
// check to see if collection (directory) exists // check to see if collection (directory) exists
//if _, err := stat(dir); err != nil { //if _, err := stat(dir); err != nil {
// return nil, err // return nil, err
//} //}
files, err := ioutil.ReadDir(dir) files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil { return nil, err }
return nil, err
}
// the IDs of collection // the IDs of collection
var recordsIDs []string var recordsIDs []string
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
if strings.HasSuffix(name, ".json") && !strings.HasPrefix(name, ".#") { if strings.HasSuffix(name, ".bson") && !strings.HasPrefix(name, ".#") {
recordsIDs = append(recordsIDs, name[:len(name)-5]) recordsIDs = append(recordsIDs, name[:len(name)-5])
} }
} }
return recordsIDs, nil return recordsIDs, nil
} }
// Delete locks that database and then attempts to remove the collection/resource // Delete locks that database and then attempts to remove the collection/resource
// specified by [path] // specified by [path]
func (d *Driver) Delete(collection, resource string) error { func (d *Driver) Delete(collection, resource string) error {
@ -264,53 +198,39 @@ func (d *Driver) Delete(collection, resource string) error {
mutex := d.getOrCreateMutex(collection) mutex := d.getOrCreateMutex(collection)
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
// //
dir := filepath.Join(d.dir, path) dir := filepath.Join(d.dir, path)
switch fi, err := stat(dir); { switch fi, err := stat(dir); {
// if fi is nil or error is not nil return // if fi is nil or error is not nil return
case fi == nil, err != nil: case fi == nil, err != nil:
return fmt.Errorf("Unable to find file or directory named %v\n", path) return fmt.Errorf("Unable to find file or directory named %v\n", path)
// remove directory and all contents // remove directory and all contents
case fi.Mode().IsDir(): case fi.Mode().IsDir():
return os.RemoveAll(dir) return os.RemoveAll(dir)
// remove file // remove file
case fi.Mode().IsRegular(): case fi.Mode().IsRegular():
return os.RemoveAll(dir + ".json") return os.RemoveAll(dir + ".bson")
} }
return nil return nil
} }
// //
func stat(path string) (fi os.FileInfo, err error) { 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 // 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) { if fi, err = os.Stat(path); os.IsNotExist(err) {
fi, err = os.Stat(path + ".json") fi, err = os.Stat(path + ".bson")
} }
return return
} }
// getOrCreateMutex creates a new collection specific mutex any time a collection // getOrCreateMutex creates a new collection specific mutex any time a collection
// is being modified to avoid unsafe operations // is being modified to avoid unsafe operations
func (d *Driver) getOrCreateMutex(collection string) *sync.Mutex { func (d *Driver) getOrCreateMutex(collection string) *sync.Mutex {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
m, ok := d.mutexes[collection] m, ok := d.mutexes[collection]
// if the mutex doesn't exist make it // if the mutex doesn't exist make it
if !ok { if !ok {
m = &sync.Mutex{} m = &sync.Mutex{}
d.mutexes[collection] = m d.mutexes[collection] = m
} }
return m return m
} }

View File

@ -1,16 +1,11 @@
package scribble package scratch
import ( import (
"os" "os"
"testing" "testing"
) )
//
type Fish struct { type Fish struct {
Type string `json:"type"` Type string `json:"type"`
} }
//
var ( var (
db *Driver db *Driver
database = "./deep/school" database = "./deep/school"
@ -20,169 +15,118 @@ var (
redfish = Fish{Type: "red"} redfish = Fish{Type: "red"}
bluefish = Fish{Type: "blue"} bluefish = Fish{Type: "blue"}
) )
//
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// remove any thing for a potentially failed previous test // remove any thing for a potentially failed previous test
os.RemoveAll("./deep") os.RemoveAll("./deep")
// run // run
code := m.Run() code := m.Run()
// cleanup // cleanup
os.RemoveAll("./deep") os.RemoveAll("./deep")
// exit // exit
os.Exit(code) os.Exit(code)
} }
// Tests creating a new database, and using an existing database // Tests creating a new database, and using an existing database
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
// database should not exist // database should not exist
if _, err := os.Stat(database); err == nil { if _, err := os.Stat(database); err == nil {
t.Error("Expected nothing, got database") t.Error("Expected nothing, got database")
} }
// create a new database // create a new database
createDB() createDB()
// database should exist // database should exist
if _, err := os.Stat(database); err != nil { if _, err := os.Stat(database); err != nil {
t.Error("Expected database, got nothing") t.Error("Expected database, got nothing")
} }
// should use existing database // should use existing database
createDB() createDB()
// database should exist // database should exist
if _, err := os.Stat(database); err != nil { if _, err := os.Stat(database); err != nil {
t.Error("Expected database, got nothing") t.Error("Expected database, got nothing")
} }
} }
//
func TestWriteAndRead(t *testing.T) { func TestWriteAndRead(t *testing.T) {
createDB() createDB()
// add fish to database // add fish to database
if err := db.Write(collection, "redfish", redfish); err != nil { if err := db.Write(collection, "redfish", redfish); err != nil {
t.Error("Create fish failed: ", err.Error()) t.Error("Create fish failed: ", err.Error())
} }
// read fish from database // read fish from database
if err := db.Read(collection, "redfish", &onefish); err != nil { if err := db.Read(collection, "redfish", &onefish); err != nil {
t.Error("Failed to read: ", err.Error()) t.Error("Failed to read: ", err.Error())
} }
// //
if onefish.Type != "red" { if onefish.Type != "red" {
t.Error("Expected red fish, got: ", onefish.Type) t.Error("Expected red fish, got: ", onefish.Type)
} }
destroySchool() destroySchool()
} }
//
func TestReadall(t *testing.T) { func TestReadall(t *testing.T) {
createDB() createDB()
createSchool() createSchool()
fish, err := db.ReadAll(collection) fish, err := db.ReadAll(collection)
if err != nil { if err != nil {
t.Error("Failed to read: ", err.Error()) t.Error("Failed to read: ", err.Error())
} }
if len(fish) <= 0 { if len(fish) <= 0 {
t.Error("Expected some fish, have none") t.Error("Expected some fish, have none")
} }
destroySchool() destroySchool()
} }
//
func TestWriteAndReadEmpty(t *testing.T) { func TestWriteAndReadEmpty(t *testing.T) {
createDB() createDB()
// create a fish with no home // create a fish with no home
if err := db.Write("", "redfish", redfish); err == nil { if err := db.Write("", "redfish", redfish); err == nil {
t.Error("Allowed write of empty resource", err.Error()) t.Error("Allowed write of empty resource", err.Error())
} }
// create a home with no fish // create a home with no fish
if err := db.Write(collection, "", redfish); err == nil { if err := db.Write(collection, "", redfish); err == nil {
t.Error("Allowed write of empty resource", err.Error()) t.Error("Allowed write of empty resource", err.Error())
} }
// no place to read // no place to read
if err := db.Read("", "redfish", onefish); err == nil { if err := db.Read("", "redfish", onefish); err == nil {
t.Error("Allowed read of empty resource", err.Error()) t.Error("Allowed read of empty resource", err.Error())
} }
destroySchool() destroySchool()
} }
//
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
createDB() createDB()
// add fish to database // add fish to database
if err := db.Write(collection, "redfish", redfish); err != nil { if err := db.Write(collection, "redfish", redfish); err != nil {
t.Error("Create fish failed: ", err.Error()) t.Error("Create fish failed: ", err.Error())
} }
// delete the fish // delete the fish
if err := db.Delete(collection, "redfish"); err != nil { if err := db.Delete(collection, "redfish"); err != nil {
t.Error("Failed to delete: ", err.Error()) t.Error("Failed to delete: ", err.Error())
} }
// read fish from database // read fish from database
if err := db.Read(collection, "redfish", &onefish); err == nil { if err := db.Read(collection, "redfish", &onefish); err == nil {
t.Error("Expected nothing, got fish") t.Error("Expected nothing, got fish")
} }
destroySchool() destroySchool()
} }
//
func TestDeleteall(t *testing.T) { func TestDeleteall(t *testing.T) {
createDB() createDB()
createSchool() createSchool()
if err := db.Delete(collection, ""); err != nil { if err := db.Delete(collection, ""); err != nil {
t.Error("Failed to delete: ", err.Error()) t.Error("Failed to delete: ", err.Error())
} }
if _, err := os.Stat(collection); err == nil { if _, err := os.Stat(collection); err == nil {
t.Error("Expected nothing, have fish") t.Error("Expected nothing, have fish")
} }
destroySchool() destroySchool()
} }
// create a new scratch database
//
// create a new scribble database
func createDB() error { func createDB() error {
var err error var err error
if db, err = New(database, nil); err != nil { if db, err = New(database, nil); err != nil {
return err return err
} }
return nil return nil
} }
// create a fish // create a fish
func createFish(fish Fish) error { func createFish(fish Fish) error {
return db.Write(collection, fish.Type, fish) return db.Write(collection, fish.Type, fish)
} }
// create many fish // create many fish
func createSchool() error { func createSchool() error {
for _, f := range []Fish{{Type: "red"}, {Type: "blue"}} { for _, f := range []Fish{{Type: "red"}, {Type: "blue"}} {
@ -190,15 +134,12 @@ func createSchool() error {
return err return err
} }
} }
return nil return nil
} }
// destroy a fish // destroy a fish
func destroyFish(name string) error { func destroyFish(name string) error {
return db.Delete(collection, name) return db.Delete(collection, name)
} }
// destroy all fish // destroy all fish
func destroySchool() error { func destroySchool() error {
return db.Delete(collection, "") return db.Delete(collection, "")