2021-09-18 01:03:09 +01:00
|
|
|
package database
|
2021-08-16 01:56:56 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/gob"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"runtime"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
|
|
|
|
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
|
2021-08-16 01:56:56 +01:00
|
|
|
bolt "go.etcd.io/bbolt"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
statusBucket string = "status"
|
|
|
|
cardBucket string = "card"
|
|
|
|
)
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
// 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
|
2021-08-16 01:56:56 +01:00
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
path = dbPath(path)
|
2021-08-16 01:56:56 +01:00
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
if err = mkDataDir(path); err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to make the data directory, %w", err)
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
2021-08-29 16:03:29 +01:00
|
|
|
opts := bolt.Options{
|
|
|
|
Timeout: 1 * time.Second,
|
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
var db *bolt.DB
|
|
|
|
|
|
|
|
if db, err = bolt.Open(path, 0o600, &opts); err != nil {
|
2021-08-16 01:56:56 +01:00
|
|
|
return nil, fmt.Errorf("unable to open database at %s, %w", path, err)
|
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
if err = ensureBuckets(db); err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to ensure the required buckets are in the database, %w", err)
|
2021-09-15 10:09:48 +01:00
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
return db, nil
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
// WriteStatusList saves one or more statuses to the status bucket.
|
|
|
|
func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
|
2021-08-16 01:56:56 +01:00
|
|
|
if len(statuses) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket := []byte(statusBucket)
|
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
err := db.Update(func(tx *bolt.Tx) error {
|
2021-08-16 01:56:56 +01:00
|
|
|
b := tx.Bucket(bucket)
|
|
|
|
|
|
|
|
if b == nil {
|
2021-09-15 10:09:48 +01:00
|
|
|
return bucketNotExistError{bucket: string(bucket)}
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, v := range statuses {
|
|
|
|
var err error
|
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
if v.ID < 1 {
|
2021-08-16 01:56:56 +01:00
|
|
|
var id uint64
|
|
|
|
if id, err = b.NextSequence(); err != nil {
|
|
|
|
return fmt.Errorf("unable to generate ID, %w", err)
|
|
|
|
}
|
2021-09-15 10:09:48 +01:00
|
|
|
v.ID = int(id)
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
encoder := gob.NewEncoder(buf)
|
|
|
|
if err = encoder.Encode(v); err != nil {
|
|
|
|
return fmt.Errorf("unable to encode data, %w", err)
|
|
|
|
}
|
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
if err = b.Put([]byte(strconv.Itoa(v.ID)), buf.Bytes()); err != nil {
|
2021-08-16 01:56:56 +01:00
|
|
|
return fmt.Errorf("unable to add the status to the bucket, %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2021-09-15 10:09:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error while saving the statuses to the database, %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
// ReadStatusList retrieves all the statuses from the status bucket.
|
|
|
|
func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
|
|
|
|
var statuses []status.Status
|
2021-09-15 10:09:48 +01:00
|
|
|
|
2021-08-16 01:56:56 +01:00
|
|
|
bucket := []byte(statusBucket)
|
|
|
|
|
|
|
|
err := db.View(func(tx *bolt.Tx) error {
|
|
|
|
b := tx.Bucket(bucket)
|
|
|
|
|
|
|
|
if b == nil {
|
2021-09-15 10:09:48 +01:00
|
|
|
return bucketNotExistError{bucket: string(bucket)}
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := b.ForEach(func(_, v []byte) error {
|
2021-09-18 01:03:09 +01:00
|
|
|
var s status.Status
|
2021-08-16 01:56:56 +01:00
|
|
|
buf := bytes.NewBuffer(v)
|
|
|
|
decoder := gob.NewDecoder(buf)
|
|
|
|
if err := decoder.Decode(&s); err != nil {
|
|
|
|
return fmt.Errorf("unable to decode data, %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
statuses = append(statuses, s)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}); err != nil {
|
2021-09-15 10:09:48 +01:00
|
|
|
return fmt.Errorf("unable to load status, %w", err)
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
2021-09-15 10:09:48 +01:00
|
|
|
|
2021-08-16 01:56:56 +01:00
|
|
|
return nil
|
|
|
|
})
|
2021-09-15 10:09:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return statuses, fmt.Errorf("error while loading statuses from the database, %w", err)
|
|
|
|
}
|
2021-08-16 01:56:56 +01:00
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
return statuses, nil
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
// WriteCard creates or updates a card to the card bucket in BoltDB.
|
|
|
|
func WriteCard(db *bolt.DB, card card.Card) (int, error) {
|
2021-08-16 01:56:56 +01:00
|
|
|
bucket := []byte(cardBucket)
|
|
|
|
|
|
|
|
err := db.Update(func(tx *bolt.Tx) error {
|
|
|
|
var err error
|
|
|
|
b := tx.Bucket(bucket)
|
|
|
|
|
|
|
|
if b == nil {
|
2021-09-15 10:09:48 +01:00
|
|
|
return bucketNotExistError{bucket: string(bucket)}
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
if card.ID < 1 {
|
2021-08-16 01:56:56 +01:00
|
|
|
var id uint64
|
|
|
|
if id, err = b.NextSequence(); err != nil {
|
|
|
|
return fmt.Errorf("unable to generate an ID for the card, %w", err)
|
|
|
|
}
|
2021-09-15 10:09:48 +01:00
|
|
|
card.ID = int(id)
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
encoder := gob.NewEncoder(buf)
|
|
|
|
if err = encoder.Encode(card); err != nil {
|
|
|
|
return fmt.Errorf("unable to encode data, %w", err)
|
|
|
|
}
|
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
if err = b.Put([]byte(strconv.Itoa(card.ID)), buf.Bytes()); err != nil {
|
2021-08-16 01:56:56 +01:00
|
|
|
return fmt.Errorf("unable to write the card to the bucket, %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2021-09-15 10:09:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return card.ID, fmt.Errorf("error while saving the card to the database, %w", err)
|
|
|
|
}
|
2021-08-16 01:56:56 +01:00
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
return card.ID, nil
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
2021-09-18 01:03:09 +01:00
|
|
|
// ReadCard reads a card from the cards bucket in BoltDB.
|
|
|
|
func ReadCard(db *bolt.DB, id int) (card.Card, error) {
|
|
|
|
var card card.Card
|
2021-09-15 10:09:48 +01:00
|
|
|
|
2021-08-16 01:56:56 +01:00
|
|
|
bucket := []byte(cardBucket)
|
|
|
|
|
2021-09-15 05:51:22 +01:00
|
|
|
err := db.View(func(tx *bolt.Tx) error {
|
2021-08-16 01:56:56 +01:00
|
|
|
b := tx.Bucket(bucket)
|
|
|
|
|
|
|
|
if b == nil {
|
2021-09-15 10:09:48 +01:00
|
|
|
return bucketNotExistError{bucket: string(bucket)}
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer(b.Get([]byte(strconv.Itoa(id))))
|
|
|
|
|
|
|
|
decoder := gob.NewDecoder(buf)
|
|
|
|
if err := decoder.Decode(&card); err != nil {
|
|
|
|
return fmt.Errorf("unable to decode card data, %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2021-09-15 10:09:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return card, fmt.Errorf("error while loading the card from the database, %w", err)
|
|
|
|
}
|
2021-08-16 01:56:56 +01:00
|
|
|
|
2021-09-15 10:09:48 +01:00
|
|
|
return card, nil
|
2021-08-16 01:56:56 +01:00
|
|
|
}
|
2021-09-18 01:03:09 +01:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|