diff --git a/LICENSE b/LICENSE index 5a15b1b..c64e27e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ - 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 of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index cc10cf7..c6b4e17 100644 --- a/README.md +++ b/README.md @@ -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 -Install using `go get github.com/sdomino/scribble`. +Install using `go get git.sdf.org/Atlas48/scratch`. ### Usage ```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 -db, err := scribble.New(dir, nil) +db, err := scratch.New(dir, nil) if err != nil { fmt.Println("Error", err) } diff --git a/scribble.go b/scratch.go similarity index 85% rename from scribble.go rename to scratch.go index 7399f9a..6ef4ec6 100644 --- a/scribble.go +++ b/scratch.go @@ -1,8 +1,7 @@ -// Package scribble is a tiny JSON database -package scribble - +// Package scratch is a tiny, flat-file BSON database +package scratch import ( - "encoding/json" + "gopkg.in/mgo.v2/bson" "errors" "fmt" "io/ioutil" @@ -10,18 +9,14 @@ import ( "path/filepath" "strings" "sync" - "github.com/jcelliott/lumber" ) - // Version is the current version of the project const Version = "1.0.4" - var ( ErrMissingResource = errors.New("missing resource - unable to save record") ErrMissingCollection = errors.New("missing collection - no place to save record") ) - type ( // Logger is a generic logger interface Logger interface { @@ -32,230 +27,169 @@ type ( Debug(string, ...interface{}) Trace(string, ...interface{}) } - - // Driver is what is used to interact with the scribble database. It runs + // Driver is what is used to interact with the scratch 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 + dir string // the directory where scratch will create the database + log Logger // the logger scratch will log to } ) - -// Options uses for specification of working golang-scribble +// Options uses for specification of working golang-scratch type Options struct { - Logger // the logger scribble will use (configurable) + Logger // the logger scratch will use (configurable) } - -// New creates a new scribble database at the desired directory location, and +// New creates a new scratch 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 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) + opts.Logger.Debug("Creating scratch database at '%s'...\n", dir) return &driver, os.MkdirAll(dir, 0755) } - // 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 ErrMissingCollection } - // ensure there is a resource (name) to save record as if resource == "" { return ErrMissingResource } - mutex := d.getOrCreateMutex(collection) mutex.Lock() defer mutex.Unlock() - // dir := filepath.Join(d.dir, collection) - fnlPath := filepath.Join(dir, resource+".json") + fnlPath := filepath.Join(dir, resource+".bson") tmpPath := fnlPath + ".tmp" - return write(dir, tmpPath, fnlPath, v) } - func write(dir, tmpPath, dstPath string, v interface{}) error { - // create collection directory if err := os.MkdirAll(dir, 0755); err != nil { return err } - // 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 { 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, dstPath) } - // 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 ErrMissingCollection } - // ensure there is a resource (name) to save record as if resource == "" { return ErrMissingResource } - // record := filepath.Join(d.dir, collection, resource) - // check to see if file exists if _, err := stat(record); err != nil { return err } - // read record from database return read(record, v) } - func read(record string, v interface{}) error { - - b, err := ioutil.ReadFile(record + ".json") - if err != nil { - return err - } - + b, err := ioutil.ReadFile(record + ".bson") + if err != nil { return err } // 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 // there is no way of knowing what type the record is. func (d *Driver) ReadAll(collection string) ([][]byte, error) { - // ensure there is a collection to read if collection == "" { return nil, ErrMissingCollection } - // dir := filepath.Join(d.dir, collection) os.MkdirAll(dir, 0777) - // read all the files in the transaction.Collection; an error here just means // the collection is either empty or doesn't exist files, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - + if 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, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - + if err != nil { return nil, err } return readAll(files, dir) } - func readAll(files []os.FileInfo, dir string) ([][]byte, error) { // the files read from the database var records [][]byte - // iterate over each of the files, attempting to read the file. If successful // append the files to the collection of read 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, b) } - // unmarhsal the read files as a comma delimeted byte array return records, nil } - // List ID of records from a collection; this is returned as a slice of strings. func (d *Driver) List(collection string) ([]string, error) { - // ensure there is a collection to read if collection == "" { return nil, ErrMissingCollection } - // dir := filepath.Join(d.dir, collection) - // check to see if collection (directory) exists //if _, err := stat(dir); err != nil { // return nil, err //} - files, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - + if err != nil { return nil, err } // the IDs of collection var recordsIDs []string - for _, file := range files { 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]) } } - return recordsIDs, nil } - // Delete locks that database and then attempts to remove the collection/resource // specified by [path] 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.Lock() defer mutex.Unlock() - // 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 os.RemoveAll(dir + ".bson") } - 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") + fi, err = os.Stat(path + ".bson") } - return } - // getOrCreateMutex creates a new collection specific mutex any time a collection // is being modified to avoid unsafe operations func (d *Driver) getOrCreateMutex(collection string) *sync.Mutex { - d.mutex.Lock() defer d.mutex.Unlock() - m, ok := d.mutexes[collection] - // if the mutex doesn't exist make it if !ok { m = &sync.Mutex{} d.mutexes[collection] = m } - return m } diff --git a/scribble_test.go b/scratch_test.go similarity index 96% rename from scribble_test.go rename to scratch_test.go index 1e4e546..991c39b 100644 --- a/scribble_test.go +++ b/scratch_test.go @@ -1,16 +1,11 @@ -package scribble - +package scratch import ( "os" "testing" ) - -// type Fish struct { Type string `json:"type"` } - -// var ( db *Driver database = "./deep/school" @@ -20,169 +15,118 @@ var ( redfish = Fish{Type: "red"} bluefish = Fish{Type: "blue"} ) - -// func TestMain(m *testing.M) { - // remove any thing for a potentially failed previous test os.RemoveAll("./deep") - // run code := m.Run() - // cleanup os.RemoveAll("./deep") - // exit os.Exit(code) } - // Tests creating a new database, and using an existing database func TestNew(t *testing.T) { - // database should not exist if _, err := os.Stat(database); err == nil { t.Error("Expected nothing, got database") } - // create a new database createDB() - // database should exist if _, err := os.Stat(database); err != nil { t.Error("Expected database, got nothing") } - // should use existing database createDB() - // database should exist if _, err := os.Stat(database); err != nil { t.Error("Expected database, got nothing") } } - -// func TestWriteAndRead(t *testing.T) { - createDB() - // add fish to database if err := db.Write(collection, "redfish", redfish); err != nil { t.Error("Create fish failed: ", err.Error()) } - // read fish from database if err := db.Read(collection, "redfish", &onefish); err != nil { t.Error("Failed to read: ", err.Error()) } - // if onefish.Type != "red" { t.Error("Expected red fish, got: ", onefish.Type) } - destroySchool() } - -// func TestReadall(t *testing.T) { - createDB() createSchool() - fish, err := db.ReadAll(collection) if err != nil { t.Error("Failed to read: ", err.Error()) } - if len(fish) <= 0 { t.Error("Expected some fish, have none") } - destroySchool() } - -// func TestWriteAndReadEmpty(t *testing.T) { - createDB() - // create a fish with no home if err := db.Write("", "redfish", redfish); err == nil { t.Error("Allowed write of empty resource", err.Error()) } - // create a home with no fish if err := db.Write(collection, "", redfish); err == nil { t.Error("Allowed write of empty resource", err.Error()) } - // no place to read if err := db.Read("", "redfish", onefish); err == nil { t.Error("Allowed read of empty resource", err.Error()) } - destroySchool() } - -// func TestDelete(t *testing.T) { - createDB() - // add fish to database if err := db.Write(collection, "redfish", redfish); err != nil { t.Error("Create fish failed: ", err.Error()) } - // delete the fish if err := db.Delete(collection, "redfish"); err != nil { t.Error("Failed to delete: ", err.Error()) } - // read fish from database if err := db.Read(collection, "redfish", &onefish); err == nil { t.Error("Expected nothing, got fish") } - destroySchool() } - -// func TestDeleteall(t *testing.T) { - createDB() createSchool() - if err := db.Delete(collection, ""); err != nil { t.Error("Failed to delete: ", err.Error()) } - if _, err := os.Stat(collection); err == nil { t.Error("Expected nothing, have fish") } - destroySchool() } - -// - -// create a new scribble database +// create a new scratch database func createDB() error { var err error if db, err = New(database, nil); err != nil { return err } - return nil } - // create a fish func createFish(fish Fish) error { return db.Write(collection, fish.Type, fish) } - // create many fish func createSchool() error { for _, f := range []Fish{{Type: "red"}, {Type: "blue"}} { @@ -190,15 +134,12 @@ func createSchool() error { return err } } - return nil } - // destroy a fish func destroyFish(name string) error { return db.Delete(collection, name) } - // destroy all fish func destroySchool() error { return db.Delete(collection, "")