153 lines
3.3 KiB
Go
153 lines
3.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
var (
|
|
bucketAppConfig = []byte("AppConfig")
|
|
bucketCategories = []byte("Categories")
|
|
bucketEntries = []byte("Entries")
|
|
bucketAdmins = []byte("Admins")
|
|
|
|
keyTocMsgID = []byte("toc_msg_id")
|
|
)
|
|
|
|
type Category struct {
|
|
Name string `json:"name"`
|
|
Order int `json:"order"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type Entry struct {
|
|
ID string `json:"id"`
|
|
Category string `json:"category"`
|
|
Title string `json:"title"`
|
|
Link string `json:"link"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
type Storage struct {
|
|
db *bolt.DB
|
|
}
|
|
|
|
func New(path string) (*Storage, error) {
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
|
}
|
|
|
|
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
s := &Storage{db: db}
|
|
if err := s.initBuckets(); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Storage) initBuckets() error {
|
|
return s.db.Update(func(tx *bolt.Tx) error {
|
|
buckets := [][]byte{bucketAppConfig, bucketCategories, bucketEntries, bucketAdmins}
|
|
for _, b := range buckets {
|
|
if _, err := tx.CreateBucketIfNotExists(b); err != nil {
|
|
return fmt.Errorf("failed to create bucket %s: %w", b, err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *Storage) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
func (s *Storage) GetTocMsgID() (int, error) {
|
|
var msgID int
|
|
err := s.db.View(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketAppConfig)
|
|
v := b.Get(keyTocMsgID)
|
|
if v != nil && len(v) >= 4 {
|
|
msgID = int(binary.BigEndian.Uint32(v))
|
|
}
|
|
return nil
|
|
})
|
|
return msgID, err
|
|
}
|
|
|
|
func (s *Storage) SetTocMsgID(msgID int) error {
|
|
return s.db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketAppConfig)
|
|
buf := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(buf, uint32(msgID))
|
|
return b.Put(keyTocMsgID, buf)
|
|
})
|
|
}
|
|
|
|
func encodeJSON(v any) ([]byte, error) {
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
func decodeJSON(data []byte, v any) error {
|
|
return json.Unmarshal(data, v)
|
|
}
|
|
|
|
// Admin management
|
|
|
|
func (s *Storage) AddAdmin(userID int64) error {
|
|
return s.db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketAdmins)
|
|
key := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(key, uint64(userID))
|
|
return b.Put(key, []byte("1"))
|
|
})
|
|
}
|
|
|
|
func (s *Storage) RemoveAdmin(userID int64) error {
|
|
return s.db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketAdmins)
|
|
key := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(key, uint64(userID))
|
|
return b.Delete(key)
|
|
})
|
|
}
|
|
|
|
func (s *Storage) IsAdmin(userID int64) bool {
|
|
var exists bool
|
|
s.db.View(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketAdmins)
|
|
key := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(key, uint64(userID))
|
|
exists = b.Get(key) != nil
|
|
return nil
|
|
})
|
|
return exists
|
|
}
|
|
|
|
func (s *Storage) ListAdmins() ([]int64, error) {
|
|
var admins []int64
|
|
err := s.db.View(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketAdmins)
|
|
return b.ForEach(func(k, v []byte) error {
|
|
if len(k) == 8 {
|
|
userID := int64(binary.BigEndian.Uint64(k))
|
|
admins = append(admins, userID)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
return admins, err
|
|
}
|