This commit is contained in:
dela
2026-02-04 22:33:45 +08:00
commit d82badc6e3
16 changed files with 2261 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
package storage
import (
"fmt"
"sort"
"time"
bolt "go.etcd.io/bbolt"
)
func (s *Storage) CreateCategory(name string, order int) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketCategories)
if b.Get([]byte(name)) != nil {
return fmt.Errorf("category %q already exists", name)
}
cat := Category{
Name: name,
Order: order,
CreatedAt: time.Now(),
}
data, err := encodeJSON(cat)
if err != nil {
return err
}
return b.Put([]byte(name), data)
})
}
func (s *Storage) DeleteCategory(name string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketCategories)
if b.Get([]byte(name)) == nil {
return fmt.Errorf("category %q not found", name)
}
return b.Delete([]byte(name))
})
}
func (s *Storage) GetCategory(name string) (*Category, error) {
var cat *Category
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketCategories)
data := b.Get([]byte(name))
if data == nil {
return fmt.Errorf("category %q not found", name)
}
cat = &Category{}
return decodeJSON(data, cat)
})
return cat, err
}
func (s *Storage) ListCategories() ([]Category, error) {
var categories []Category
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketCategories)
return b.ForEach(func(k, v []byte) error {
var cat Category
if err := decodeJSON(v, &cat); err != nil {
return err
}
categories = append(categories, cat)
return nil
})
})
if err != nil {
return nil, err
}
sort.Slice(categories, func(i, j int) bool {
if categories[i].Order != categories[j].Order {
return categories[i].Order < categories[j].Order
}
return categories[i].CreatedAt.Before(categories[j].CreatedAt)
})
return categories, nil
}
func (s *Storage) UpdateCategoryOrder(name string, order int) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketCategories)
data := b.Get([]byte(name))
if data == nil {
return fmt.Errorf("category %q not found", name)
}
var cat Category
if err := decodeJSON(data, &cat); err != nil {
return err
}
cat.Order = order
newData, err := encodeJSON(cat)
if err != nil {
return err
}
return b.Put([]byte(name), newData)
})
}
func (s *Storage) CategoryExists(name string) bool {
exists := false
s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketCategories)
exists = b.Get([]byte(name)) != nil
return nil
})
return exists
}

147
internal/storage/entry.go Normal file
View File

@@ -0,0 +1,147 @@
package storage
import (
"fmt"
"sort"
"time"
"github.com/teris-io/shortid"
bolt "go.etcd.io/bbolt"
)
func (s *Storage) CreateEntry(category, title, link string) (*Entry, error) {
entry := &Entry{
ID: shortid.MustGenerate(),
Category: category,
Title: title,
Link: link,
Timestamp: time.Now(),
}
err := s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketEntries)
data, err := encodeJSON(entry)
if err != nil {
return err
}
return b.Put([]byte(entry.ID), data)
})
if err != nil {
return nil, err
}
return entry, nil
}
func (s *Storage) GetEntry(id string) (*Entry, error) {
var entry *Entry
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketEntries)
data := b.Get([]byte(id))
if data == nil {
return fmt.Errorf("entry %q not found", id)
}
entry = &Entry{}
return decodeJSON(data, entry)
})
return entry, err
}
func (s *Storage) DeleteEntry(id string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketEntries)
if b.Get([]byte(id)) == nil {
return fmt.Errorf("entry %q not found", id)
}
return b.Delete([]byte(id))
})
}
func (s *Storage) UpdateEntryTitle(id, title string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketEntries)
data := b.Get([]byte(id))
if data == nil {
return fmt.Errorf("entry %q not found", id)
}
var entry Entry
if err := decodeJSON(data, &entry); err != nil {
return err
}
entry.Title = title
newData, err := encodeJSON(entry)
if err != nil {
return err
}
return b.Put([]byte(id), newData)
})
}
func (s *Storage) UpdateEntryCategory(id, category string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketEntries)
data := b.Get([]byte(id))
if data == nil {
return fmt.Errorf("entry %q not found", id)
}
var entry Entry
if err := decodeJSON(data, &entry); err != nil {
return err
}
entry.Category = category
newData, err := encodeJSON(entry)
if err != nil {
return err
}
return b.Put([]byte(id), newData)
})
}
func (s *Storage) ListEntries(category string) ([]Entry, error) {
var entries []Entry
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketEntries)
return b.ForEach(func(k, v []byte) error {
var entry Entry
if err := decodeJSON(v, &entry); err != nil {
return err
}
if category == "" || entry.Category == category {
entries = append(entries, entry)
}
return nil
})
})
if err != nil {
return nil, err
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Timestamp.Before(entries[j].Timestamp)
})
return entries, nil
}
func (s *Storage) ListAllEntries() ([]Entry, error) {
return s.ListEntries("")
}
func (s *Storage) GetEntriesByCategory() (map[string][]Entry, error) {
entries, err := s.ListAllEntries()
if err != nil {
return nil, err
}
result := make(map[string][]Entry)
for _, e := range entries {
result[e.Category] = append(result[e.Category], e)
}
return result, nil
}

104
internal/storage/storage.go Normal file
View File

@@ -0,0 +1,104 @@
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")
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}
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)
}