pelican/internal/database/database.go
Dan Anglin 0e186be66b
refactor: create BoltItem interface
Create a BoltItem interface which is used to make
the database fucntions more generic.

As part of this change, the Status and Card types
have migrated back into the board package.
2021-09-23 21:21:44 +01:00

257 lines
5.8 KiB
Go

package database
import (
"bytes"
"encoding/gob"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
bolt "go.etcd.io/bbolt"
)
const (
StatusBucket string = "status"
CardBucket string = "card"
)
type BoltItem interface {
UpdateId(int)
Id() int
}
// OpenDatabase opens the database, at a given path, for reading and writing.
// If the file does not exist it will be created.
func OpenDatabase(path string) (*bolt.DB, error) {
var err error
path = dbPath(path)
if err = mkDataDir(path); err != nil {
return nil, fmt.Errorf("unable to make the data directory, %w", err)
}
opts := bolt.Options{
Timeout: 1 * time.Second,
}
var db *bolt.DB
if db, err = bolt.Open(path, 0o600, &opts); err != nil {
return nil, fmt.Errorf("unable to open database at %s, %w", path, err)
}
if err = ensureBuckets(db); err != nil {
return nil, fmt.Errorf("unable to ensure the required buckets are in the database, %w", err)
}
return db, nil
}
// WriteMany saves one or more statuses to the status bucket.
func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) error {
if len(items) == 0 {
return nil
}
bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
for _, i := range items {
var err error
if i.Id() < 1 {
var id uint64
if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate ID, %w", err)
}
i.UpdateId(int(id))
}
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
if err = encoder.Encode(i); err != nil {
return fmt.Errorf("unable to encode data, %w", err)
}
if err = b.Put([]byte(strconv.Itoa(i.Id())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to add the status to the bucket, %w", err)
}
}
return nil
})
if err != nil {
return fmt.Errorf("error while saving the statuses to the database, %w", err)
}
return nil
}
// TODO: Create ReadMany
// ReadAll retrieves all the statuses from the status bucket.
func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
bucket := []byte(bucketName)
var output [][]byte
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
if err := b.ForEach(func(_, v []byte) error {
output = append(output, v)
return nil
}); err != nil {
return fmt.Errorf("unable to load status, %w", err)
}
return nil
})
if err != nil {
return output, fmt.Errorf("error while loading statuses from the database, %w", err)
}
return output, nil
}
// Write creates or updates a Bolt item to a specified bucket.
func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) {
bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error {
var err error
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
if item.Id() < 1 {
var id uint64
if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate an ID for the card, %w", err)
}
item.UpdateId(int(id))
}
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
if err = encoder.Encode(item); err != nil {
return fmt.Errorf("unable to encode data, %w", err)
}
if err = b.Put([]byte(strconv.Itoa(item.Id())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to write the card to the bucket, %w", err)
}
return nil
})
if err != nil {
return 0, fmt.Errorf("error while saving the card to the database, %w", err)
}
return item.Id(), nil
}
// Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) {
bucket := []byte(bucketName)
var data []byte
if err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
data = b.Get([]byte(strconv.Itoa(id)))
return nil
}); err != nil {
return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err)
}
return data, nil
}
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.
// For linux, the default location of the database file is $XDG_DATA_HOME/pelican/pelican.db. If the XDG_DATA_HOME environment
// variable is not set then it will default to $HOME/.local/share/pelican/pelican.db. For all other operating systems the default
// location is $HOME/.pelican/pelican.db.
func dbPath(path string) string {
if len(path) > 0 {
filepath.Dir(path)
return path
}
dbFilename := "pelican.db"
var dataDir string
goos := runtime.GOOS
switch goos {
case "linux":
dataHome := os.Getenv("XDG_DATA_HOME")
if len(dataHome) == 0 {
dataHome = filepath.Join(os.Getenv("HOME"), ".local", "share")
}
dataDir = filepath.Join(dataHome, "pelican")
default:
dataDir = filepath.Join(os.Getenv("HOME"), ".pelican")
}
path = filepath.Join(dataDir, dbFilename)
return path
}
// mkDataDir creates the data directory of a given path to the database.
func mkDataDir(path string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("error while making directory %s, %w", dir, err)
}
return nil
}
// ensureBuckets ensures that the required buckets are created in the database.
func ensureBuckets(db *bolt.DB) error {
buckets := []string{StatusBucket, CardBucket}
err := db.Update(func(tx *bolt.Tx) error {
for _, v := range buckets {
if _, err := tx.CreateBucketIfNotExists([]byte(v)); err != nil {
return fmt.Errorf("unable to ensure that %s bucket is created in the database, %w", v, err)
}
}
return nil
})
if err != nil {
return fmt.Errorf("error while ensuring buckets exist in the database, %w", err)
}
return nil
}