blog/core/jsondb/jsondb.go

188 lines
4.0 KiB
Go

// Package jsondb implements a flat file JSON database engine.
package jsondb
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// DB is the database manager.
type DB struct {
Root string // The root directory of the database
// Use Redis to cache filesystem reads of the database.
EnableRedis bool
RedisURL string
}
// Error codes returned.
var (
ErrNotFound = errors.New("document not found")
)
// New initializes the JSON database.
func New(root string) *DB {
return &DB{
Root: root,
}
}
// Get a document by path and load it into the object `v`.
func (db *DB) Get(document string, v interface{}) error {
log.Debug("GET %s", document)
if !db.Exists(document) {
return ErrNotFound
}
// Get the file path and stats.
path := db.toPath(document)
_, err := os.Stat(path) // TODO: mtime for caching
if err != nil {
return err
}
// Read the JSON.
err = db.readJSON(path, &v)
if err != nil {
return err
}
return nil
}
// Commit writes a JSON object to the database.
func (db *DB) Commit(document string, v interface{}) error {
log.Debug("COMMIT %s", document)
path := db.toPath(document)
// Ensure the directory tree is ready.
db.makePath(path)
// Write the document.
err := db.writeJSON(path, v)
if err != nil {
return err
}
return nil
}
// Delete removes a JSON document from the database.
func (db *DB) Delete(document string) error {
log.Debug("DELETE %s", document)
path := db.toPath(document)
if _, err := os.Stat(path); os.IsNotExist(err) {
log.Warn("Delete document %s: did not exist")
return nil
}
return os.Remove(path)
}
// Exists tells you whether a document exists in the database.
func (db *DB) Exists(document string) bool {
if _, err := os.Stat(db.toPath(document)); os.IsNotExist(err) {
return false
}
return true
}
// List all the documents at the path given.
func (db *DB) List(path string) ([]string, error) {
return db.list(path, false)
}
// ListAll recursively lists the documents at the path prefix given.
func (db *DB) ListAll(path string) ([]string, error) {
return db.list(path, true)
}
// makePath ensures all the directory components in a document path exist.
// path: the filesystem path like from toPath().
func (db *DB) makePath(path string) error {
parts := strings.Split(path, string(filepath.Separator))
log.Debug("%v", parts)
parts = parts[:len(parts)-1] // pop off the filename
log.Debug("%v", parts)
directory := filepath.Join(parts...)
log.Debug("Ensure exists: %s (from orig path %s)", directory, path)
if _, err := os.Stat(directory); err != nil {
log.Debug("Create directory: %s", directory)
err = os.MkdirAll(directory, 0755)
return err
}
return nil
}
// list returns the documents under a path with optional recursion.
func (db *DB) list(path string, recursive bool) ([]string, error) {
root := filepath.Join(db.Root, path)
var docs []string
files, err := ioutil.ReadDir(root)
if err != nil {
return docs, err
}
for _, file := range files {
filePath := filepath.Join(root, file.Name())
dbPath := filepath.Join(path, file.Name())
if file.IsDir() && recursive {
subfiles, err := db.list(dbPath, recursive)
if err != nil {
return docs, err
}
docs = append(docs, subfiles...)
continue
}
if strings.HasSuffix(filePath, ".json") {
name := strings.TrimSuffix(filePath, ".json")
docs = append(docs, name)
}
}
return docs, nil
}
// readJSON reads a JSON file from disk.
func (db *DB) readJSON(path string, v interface{}) error {
fh, err := os.Open(path)
if err != nil {
return err
}
defer fh.Close()
decoder := json.NewDecoder(fh)
err = decoder.Decode(&v)
return err
}
// writeJSON writes a JSON document to disk.
func (db *DB) writeJSON(path string, v interface{}) error {
fh, err := os.Create(path)
if err != nil {
return err
}
defer fh.Close()
encoder := json.NewEncoder(fh)
encoder.SetIndent("", "\t")
encoder.Encode(v)
return nil
}
// toPath translates a document name into a filesystem path.
func (db *DB) toPath(document string) string {
return filepath.Join(db.Root, document+".json")
}