diff --git a/init.go b/init.go new file mode 100644 index 0000000..69344e9 --- /dev/null +++ b/init.go @@ -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) +} diff --git a/io.go b/io.go new file mode 100644 index 0000000..e90d1b9 --- /dev/null +++ b/io.go @@ -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 +} diff --git a/read.go b/read.go new file mode 100644 index 0000000..9aab6a9 --- /dev/null +++ b/read.go @@ -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 +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..5b8d6b0 --- /dev/null +++ b/types.go @@ -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) + } +) diff --git a/write.go b/write.go new file mode 100644 index 0000000..13f7289 --- /dev/null +++ b/write.go @@ -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 +}