pelican/internal/db/database.go
Dan Anglin 5189ebe7bb
All checks were successful
/ test (pull_request) Successful in 40s
/ lint (pull_request) Successful in 42s
refactor: remove duplicate write function in db
Remove the original 'Write' function from the db package and rename the
'WriteMany' function to 'Write' as it can already write one or many Bolt
items to the database.

Also update the test suites and add more coverage in the board package.
2024-01-23 18:31:01 +00:00

243 lines
5.6 KiB
Go

package db
import (
"bytes"
"encoding/gob"
"fmt"
"os"
"path/filepath"
"strconv"
"time"
bolt "go.etcd.io/bbolt"
)
const (
StatusBucket string = "status"
CardBucket string = "card"
)
type dbWriteMode int
const (
WriteModeCreate dbWriteMode = iota
WriteModeUpdate
)
type BoltItem interface {
SetID(int) error
GetID() 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
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 database *bolt.DB
if database, err = bolt.Open(path, 0o600, &opts); err != nil {
return nil, fmt.Errorf("unable to open database at %s, %w", path, err)
}
if err = ensureBuckets(database); err != nil {
return nil, fmt.Errorf("unable to ensure the required buckets are in the database, %w", err)
}
return database, nil
}
// Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
func Read(db *bolt.DB, bucketName string, itemID int) ([]byte, error) {
bucketNameBytes := []byte(bucketName)
var data []byte
if err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bucketNameBytes)
if bucket == nil {
return bucketNotExistError{bucket: string(bucketNameBytes)}
}
data = bucket.Get([]byte(strconv.Itoa(itemID)))
return nil
}); err != nil {
return nil, fmt.Errorf("error while reading the Bolt item from the database; %w", err)
}
return data, nil
}
// ReadMany reads one or more Bolt items from the specified bucket.
func ReadMany(db *bolt.DB, bucketName string, ids []int) ([][]byte, error) {
bucketNameBytes := []byte(bucketName)
var output [][]byte
err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bucketNameBytes)
if bucket == nil {
return bucketNotExistError{bucket: bucketName}
}
for _, v := range ids {
data := bucket.Get([]byte(strconv.Itoa(v)))
output = append(output, data)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error while retrieving the data from the database, %w", err)
}
return output, nil
}
// ReadAll retrieves all the Bolt Items from the specified bucket.
func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
bucketNameBytes := []byte(bucketName)
var output [][]byte
err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bucketNameBytes)
if bucket == nil {
return bucketNotExistError{bucket: bucketName}
}
if err := bucket.ForEach(func(_, v []byte) error {
output = append(output, v)
return nil
}); err != nil {
return fmt.Errorf("unable to load the Bolt item; %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error while loading the Bolt items from the database; %w", err)
}
return output, nil
}
// Write saves one or more Bolt items to the status bucket.
func Write(database *bolt.DB, writeMode dbWriteMode, bucketName string, items []BoltItem) ([]int, error) {
if len(items) == 0 {
return nil, itemListEmptyError{}
}
ids := make([]int, len(items))
bucketNameBytes := []byte(bucketName)
err := database.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bucketNameBytes)
if bucket == nil {
return bucketNotExistError{bucket: string(bucketNameBytes)}
}
for ind, item := range items {
var err error
if writeMode == WriteModeCreate && item.GetID() < 1 {
var id uint64
if id, err = bucket.NextSequence(); err != nil {
return fmt.Errorf("unable to generate ID, %w", err)
}
if err = item.SetID(int(id)); err != nil {
return fmt.Errorf("unable to set the generated ID to the Bolt item; %w", err)
}
}
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 = bucket.Put([]byte(strconv.Itoa(item.GetID())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to add the Bolt Item to the %s bucket, %w", bucketName, err)
}
ids[ind] = item.GetID()
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error while saving the Bolt Items to the database, %w", err)
}
return ids, nil
}
// Delete deletes a Bolt item from a specified bucket.
func Delete(db *bolt.DB, bucketName string, itemID int) error {
bucketNameBytes := []byte(bucketName)
if err := db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(bucketNameBytes)
if bucket == nil {
return bucketNotExistError{bucket: bucketName}
}
if err := bucket.Delete([]byte(strconv.Itoa(itemID))); err != nil {
return fmt.Errorf("an error occurred when deleting Bolt item '%d'; %w", itemID, err)
}
return nil
}); err != nil {
return fmt.Errorf("error deleting data from the '%s' bucket; %w", bucketName, err)
}
return nil
}
// 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
}