package db
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
keyPerm = os.FileMode(0600)
|
|
dirPerm = os.FileMode(0700)
|
|
)
|
|
|
|
func init() {
|
|
registerDBCreator(FSDBBackendStr, func(name string, dir string) (DB, error) {
|
|
dbPath := filepath.Join(dir, name+".db")
|
|
return NewFSDB(dbPath), nil
|
|
}, false)
|
|
}
|
|
|
|
var _ DB = (*FSDB)(nil)
|
|
|
|
// It's slow.
|
|
type FSDB struct {
|
|
mtx sync.Mutex
|
|
dir string
|
|
}
|
|
|
|
func NewFSDB(dir string) *FSDB {
|
|
err := os.MkdirAll(dir, dirPerm)
|
|
if err != nil {
|
|
panic(errors.Wrap(err, "Creating FSDB dir "+dir))
|
|
}
|
|
database := &FSDB{
|
|
dir: dir,
|
|
}
|
|
return database
|
|
}
|
|
|
|
func (db *FSDB) Get(key []byte) []byte {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
panicNilKey(key)
|
|
|
|
path := db.nameToPath(key)
|
|
value, err := read(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
panic(errors.Wrapf(err, "Getting key %s (0x%X)", string(key), key))
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (db *FSDB) Has(key []byte) bool {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
panicNilKey(key)
|
|
|
|
path := db.nameToPath(key)
|
|
_, err := read(path)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
} else if err != nil {
|
|
panic(errors.Wrapf(err, "Getting key %s (0x%X)", string(key), key))
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (db *FSDB) Set(key []byte, value []byte) {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
panicNilKey(key)
|
|
|
|
db.SetNoLock(key, value)
|
|
}
|
|
|
|
func (db *FSDB) SetSync(key []byte, value []byte) {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
panicNilKey(key)
|
|
|
|
db.SetNoLock(key, value)
|
|
}
|
|
|
|
// NOTE: Implements atomicSetDeleter.
|
|
func (db *FSDB) SetNoLock(key []byte, value []byte) {
|
|
panicNilKey(key)
|
|
if value == nil {
|
|
value = []byte{}
|
|
}
|
|
path := db.nameToPath(key)
|
|
err := write(path, value)
|
|
if err != nil {
|
|
panic(errors.Wrapf(err, "Setting key %s (0x%X)", string(key), key))
|
|
}
|
|
}
|
|
|
|
func (db *FSDB) Delete(key []byte) {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
panicNilKey(key)
|
|
|
|
db.DeleteNoLock(key)
|
|
}
|
|
|
|
func (db *FSDB) DeleteSync(key []byte) {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
panicNilKey(key)
|
|
|
|
db.DeleteNoLock(key)
|
|
}
|
|
|
|
// NOTE: Implements atomicSetDeleter.
|
|
func (db *FSDB) DeleteNoLock(key []byte) {
|
|
panicNilKey(key)
|
|
path := db.nameToPath(key)
|
|
err := remove(path)
|
|
if os.IsNotExist(err) {
|
|
return
|
|
} else if err != nil {
|
|
panic(errors.Wrapf(err, "Removing key %s (0x%X)", string(key), key))
|
|
}
|
|
}
|
|
|
|
func (db *FSDB) Close() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
func (db *FSDB) Print() {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
|
|
panic("FSDB.Print not yet implemented")
|
|
}
|
|
|
|
func (db *FSDB) Stats() map[string]string {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
|
|
panic("FSDB.Stats not yet implemented")
|
|
}
|
|
|
|
func (db *FSDB) NewBatch() Batch {
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
|
|
// Not sure we would ever want to try...
|
|
// It doesn't seem easy for general filesystems.
|
|
panic("FSDB.NewBatch not yet implemented")
|
|
}
|
|
|
|
func (db *FSDB) Mutex() *sync.Mutex {
|
|
return &(db.mtx)
|
|
}
|
|
|
|
func (db *FSDB) Iterator(start, end []byte) Iterator {
|
|
it := newMemDBIterator(db, start, end)
|
|
|
|
db.mtx.Lock()
|
|
defer db.mtx.Unlock()
|
|
|
|
// We need a copy of all of the keys.
|
|
// Not the best, but probably not a bottleneck depending.
|
|
keys, err := list(db.dir, start, end)
|
|
if err != nil {
|
|
panic(errors.Wrapf(err, "Listing keys in %s", db.dir))
|
|
}
|
|
sort.Strings(keys)
|
|
it.keys = keys
|
|
return it
|
|
}
|
|
|
|
func (db *FSDB) ReverseIterator(start, end []byte) Iterator {
|
|
// XXX
|
|
return nil
|
|
}
|
|
|
|
func (db *FSDB) nameToPath(name []byte) string {
|
|
n := url.PathEscape(string(name))
|
|
return filepath.Join(db.dir, n)
|
|
}
|
|
|
|
// Read some bytes to a file.
|
|
// CONTRACT: returns os errors directly without wrapping.
|
|
func read(path string) ([]byte, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
d, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// Write some bytes from a file.
|
|
// CONTRACT: returns os errors directly without wrapping.
|
|
func write(path string, d []byte) error {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, keyPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = f.Write(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = f.Sync()
|
|
return err
|
|
}
|
|
|
|
// Remove a file.
|
|
// CONTRACT: returns os errors directly without wrapping.
|
|
func remove(path string) error {
|
|
return os.Remove(path)
|
|
}
|
|
|
|
// List files of a path.
|
|
// Paths will NOT include dir as the prefix.
|
|
// CONTRACT: returns os errors directly without wrapping.
|
|
func list(dirPath string, start, end []byte) ([]string, error) {
|
|
dir, err := os.Open(dirPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer dir.Close()
|
|
|
|
names, err := dir.Readdirnames(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var paths []string
|
|
for _, name := range names {
|
|
n, err := url.PathUnescape(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to unescape %s while listing", name)
|
|
}
|
|
if IsKeyInDomain(n, start, end) {
|
|
paths = append(paths, n)
|
|
}
|
|
}
|
|
return paths, nil
|
|
}
|