package main import ( "bytes" "encoding/gob" "fmt" "os" "path/filepath" "runtime" "strconv" "time" bolt "go.etcd.io/bbolt" ) const ( statusBucket string = "status" cardBucket string = "card" ) func mkDataDir(dir string) error { if err := os.MkdirAll(dir, 0o700); err != nil { return fmt.Errorf("error while making directory %s, %w", dir, err) } return 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. // // Before returning the path, dbPath attempts to create the directory of the database file. An error is returned if there // are any issues creating this directory. func dbPath(path string) (string, error) { if len(path) > 0 { filepath.Dir(path) return path, mkDataDir(filepath.Dir(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") } if err := os.MkdirAll(dataDir, 0o700); err != nil { return "", fmt.Errorf("unable to make directory %s, %w", dataDir, err) } path = filepath.Join(dataDir, dbFilename) return path, mkDataDir(dataDir) } // 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) { opts := bolt.Options{ Timeout: 1 * time.Second, } db, err := bolt.Open(path, 0o600, &opts) if err != nil { return nil, fmt.Errorf("unable to open database at %s, %w", path, err) } return db, 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 } // saveStatuses saves one or more statuses to the status bucket. func saveStatuses(db *bolt.DB, statuses []Status) error { if len(statuses) == 0 { return nil } bucket := []byte(statusBucket) err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket(bucket) if b == nil { return bucketNotExistError{bucket: string(bucket)} } for _, v := range statuses { var err error if v.ID < 1 { var id uint64 if id, err = b.NextSequence(); err != nil { return fmt.Errorf("unable to generate ID, %w", err) } v.ID = int(id) } buf := new(bytes.Buffer) encoder := gob.NewEncoder(buf) if err = encoder.Encode(v); err != nil { return fmt.Errorf("unable to encode data, %w", err) } if err = b.Put([]byte(strconv.Itoa(v.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 } // loadAllStatuses retrieves all the statuses from the status bucket. func loadAllStatuses(db *bolt.DB) ([]Status, error) { var statuses []Status bucket := []byte(statusBucket) 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 { var s Status 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 { return fmt.Errorf("unable to load status, %w", err) } return nil }) if err != nil { return statuses, fmt.Errorf("error while loading statuses from the database, %w", err) } return statuses, nil } // saveCard writes a card to the card bucket in BoltDB. func saveCard(db *bolt.DB, card Card) (int, error) { bucket := []byte(cardBucket) err := db.Update(func(tx *bolt.Tx) error { var err error b := tx.Bucket(bucket) if b == nil { return bucketNotExistError{bucket: string(bucket)} } if card.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) } card.ID = int(id) } buf := new(bytes.Buffer) encoder := gob.NewEncoder(buf) if err = encoder.Encode(card); err != nil { return fmt.Errorf("unable to encode data, %w", err) } if err = b.Put([]byte(strconv.Itoa(card.ID)), buf.Bytes()); err != nil { return fmt.Errorf("unable to write the card to the bucket, %w", err) } return nil }) if err != nil { return card.ID, fmt.Errorf("error while saving the card to the database, %w", err) } return card.ID, nil } // loadCard retrieves a card from the cards bucket in BoltDB. func loadCard(db *bolt.DB, id int) (Card, error) { var card Card bucket := []byte(cardBucket) err := db.View(func(tx *bolt.Tx) error { b := tx.Bucket(bucket) if b == nil { return bucketNotExistError{bucket: string(bucket)} } 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 }) if err != nil { return card, fmt.Errorf("error while loading the card from the database, %w", err) } return card, nil }