mirror of
https://github.com/skidoodle/pastebin
synced 2026-04-28 03:07:40 +02:00
small refactor
This commit is contained in:
+51
-56
@@ -2,26 +2,20 @@ package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var pastesBucket = []byte("pastes")
|
||||
var (
|
||||
pastesBucket = []byte("pastes")
|
||||
hashesBucket = []byte("hashes")
|
||||
)
|
||||
|
||||
// Paste represents the data stored for each paste.
|
||||
type Paste struct {
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// BoltStore is a bbolt implementation of the Store interface.
|
||||
type BoltStore struct {
|
||||
db *bbolt.DB
|
||||
}
|
||||
|
||||
// NewBoltStore creates a new BoltStore and initializes the database.
|
||||
func NewBoltStore(path string) (*BoltStore, error) {
|
||||
db, err := bbolt.Open(path, 0600, nil)
|
||||
if err != nil {
|
||||
@@ -29,7 +23,10 @@ func NewBoltStore(path string) (*BoltStore, error) {
|
||||
}
|
||||
|
||||
err = db.Update(func(tx *bbolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists(pastesBucket)
|
||||
if _, err := tx.CreateBucketIfNotExists(pastesBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.CreateBucketIfNotExists(hashesBucket)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -39,96 +36,94 @@ func NewBoltStore(path string) (*BoltStore, error) {
|
||||
return &BoltStore{db: db}, nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from the store.
|
||||
func (s *BoltStore) Get(key string) (string, bool, error) {
|
||||
func (s *BoltStore) Get(id string) (*Paste, bool, error) {
|
||||
var paste Paste
|
||||
exists := false
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(pastesBucket)
|
||||
val := b.Get([]byte(key))
|
||||
val := b.Get([]byte(id))
|
||||
if val == nil {
|
||||
return nil // Not found
|
||||
return nil
|
||||
}
|
||||
exists = true
|
||||
return json.Unmarshal(val, &paste)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
return nil, false, err
|
||||
}
|
||||
if paste.Content == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
return paste.Content, true, nil
|
||||
return &paste, exists, nil
|
||||
}
|
||||
|
||||
// Set adds a value to the store with a timestamp.
|
||||
func (s *BoltStore) Set(key, value string) error {
|
||||
func (s *BoltStore) GetIDByHash(hash string) (string, bool, error) {
|
||||
var id string
|
||||
exists := false
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(hashesBucket)
|
||||
val := b.Get([]byte(hash))
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
exists = true
|
||||
id = string(val)
|
||||
return nil
|
||||
})
|
||||
return id, exists, err
|
||||
}
|
||||
|
||||
func (s *BoltStore) Set(id, hash, content string) error {
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(pastesBucket)
|
||||
pb := tx.Bucket(pastesBucket)
|
||||
hb := tx.Bucket(hashesBucket)
|
||||
|
||||
paste := Paste{
|
||||
Content: value,
|
||||
Content: content,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
encoded, err := json.Marshal(paste)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(key), encoded)
|
||||
|
||||
if err := pb.Put([]byte(id), encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
return hb.Put([]byte(hash), []byte(id))
|
||||
})
|
||||
}
|
||||
|
||||
// Del removes a value from the store.
|
||||
func (s *BoltStore) Del(key string) error {
|
||||
func (s *BoltStore) Del(id string) error {
|
||||
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(pastesBucket)
|
||||
return b.Delete([]byte(key))
|
||||
return tx.Bucket(pastesBucket).Delete([]byte(id))
|
||||
})
|
||||
}
|
||||
|
||||
// Cleanup iterates through all pastes and deletes those older than maxAge.
|
||||
func (s *BoltStore) Cleanup(maxAge time.Duration) {
|
||||
slog.Info("running cleanup for old pastes")
|
||||
var keysToDelete [][]byte
|
||||
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||
s.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(pastesBucket)
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
var paste Paste
|
||||
if err := json.Unmarshal(v, &paste); err != nil {
|
||||
slog.Error("failed to unmarshal paste during cleanup", "key", string(k), "error", err)
|
||||
continue
|
||||
}
|
||||
if time.Since(paste.CreatedAt) > maxAge {
|
||||
keysToDelete = append(keysToDelete, k)
|
||||
var p Paste
|
||||
if err := json.Unmarshal(v, &p); err == nil {
|
||||
if time.Since(p.CreatedAt) > maxAge {
|
||||
keysToDelete = append(keysToDelete, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("failed to view pastes for cleanup", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(keysToDelete) > 0 {
|
||||
err = s.db.Update(func(tx *bbolt.Tx) error {
|
||||
s.db.Update(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(pastesBucket)
|
||||
for _, k := range keysToDelete {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
b.Delete(k)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("failed to delete old pastes", "error", err)
|
||||
} else {
|
||||
slog.Info("cleanup finished", "deleted_count", len(keysToDelete))
|
||||
}
|
||||
} else {
|
||||
slog.Info("no old pastes to delete")
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
func (s *BoltStore) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
+62
-77
@@ -1,97 +1,82 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// setupTestDB creates a temporary BoltDB instance for testing and returns a cleanup function.
|
||||
func setupTestDB(t *testing.T) (*BoltStore, func()) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "test.db")
|
||||
store, err := NewBoltStore(dbPath)
|
||||
assert.NoError(t, err)
|
||||
func TestBoltStore(t *testing.T) {
|
||||
dbPath := "test.db"
|
||||
defer os.Remove(dbPath)
|
||||
|
||||
cleanup := func() {
|
||||
if err := store.Close(); err != nil {
|
||||
slog.Error("failed to close store", "error", err)
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
slog.Error("failed to remove temp directory", "dir", dir, "error", err)
|
||||
}
|
||||
}
|
||||
s, err := NewBoltStore(dbPath)
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
return store, cleanup
|
||||
}
|
||||
t.Run("Set and Get", func(t *testing.T) {
|
||||
id := "id1"
|
||||
hash := "hash1"
|
||||
content := "content1"
|
||||
|
||||
// TestBoltStore_SetGetDel tests the Set, Get, and Del methods of the BoltStore.
|
||||
func TestBoltStore_SetGetDel(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
err := s.Set(id, hash, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test Set and Get
|
||||
err := store.Set("key1", "value1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
val, ok, err := store.Get("key1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "value1", val)
|
||||
|
||||
// Test Get non-existent key
|
||||
_, ok, err = store.Get("non-existent")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
// Test Delete
|
||||
err = store.Del("key1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, ok, err = store.Get("key1")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
// TestBoltStore_Cleanup tests the Cleanup method of the BoltStore.
|
||||
func TestBoltStore_Cleanup(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Set one fresh and one stale paste
|
||||
err := store.Set("fresh", "content")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = store.db.Update(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket(pastesBucket)
|
||||
stalePaste := Paste{
|
||||
Content: "stale",
|
||||
CreatedAt: time.Now().Add(-2 * time.Hour),
|
||||
}
|
||||
encoded, err := json.Marshal(stalePaste)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte("stale"), encoded)
|
||||
p, exists, err := s.Get(id)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, content, p.Content)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Cleanup pastes older than 1 hour
|
||||
store.Cleanup(1 * time.Hour)
|
||||
t.Run("GetIDByHash", func(t *testing.T) {
|
||||
id := "id2"
|
||||
hash := "hash2"
|
||||
content := "content2"
|
||||
|
||||
// Check that fresh paste still exists
|
||||
_, ok, err := store.Get("fresh")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
s.Set(id, hash, content)
|
||||
storedID, exists, err := s.GetIDByHash(hash)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, id, storedID)
|
||||
})
|
||||
|
||||
// Check that stale paste was deleted
|
||||
_, ok, err = store.Get("stale")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
t.Run("Del", func(t *testing.T) {
|
||||
id := "id3"
|
||||
s.Set(id, "h3", "c3")
|
||||
err := s.Del(id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, exists, _ := s.Get(id)
|
||||
assert.False(t, exists)
|
||||
})
|
||||
|
||||
t.Run("Cleanup", func(t *testing.T) {
|
||||
s.Set("old", "oldhash", "oldcontent")
|
||||
s.Cleanup(-time.Hour)
|
||||
_, exists, _ := s.Get("old")
|
||||
assert.False(t, exists)
|
||||
})
|
||||
|
||||
t.Run("Cleanup Bad Data", func(t *testing.T) {
|
||||
s.db.Update(func(tx *bbolt.Tx) error {
|
||||
return tx.Bucket(pastesBucket).Put([]byte("bad"), []byte("invalid json"))
|
||||
})
|
||||
s.Cleanup(-time.Hour)
|
||||
// Should skip without panic
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewBoltStoreError(t *testing.T) {
|
||||
// Use a directory name as file path to trigger error
|
||||
err := os.Mkdir("testdir", 0755)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll("testdir")
|
||||
|
||||
s, err := NewBoltStore("testdir")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, s)
|
||||
}
|
||||
|
||||
+24
-15
@@ -2,41 +2,50 @@ package store
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MemoryStore is an in-memory implementation of the Store interface.
|
||||
type MemoryStore struct {
|
||||
data map[string]string
|
||||
mu sync.RWMutex
|
||||
pastes map[string]*Paste
|
||||
hashes map[string]string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMemoryStore creates a new MemoryStore.
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
return &MemoryStore{
|
||||
data: make(map[string]string),
|
||||
pastes: make(map[string]*Paste),
|
||||
hashes: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from the store.
|
||||
func (s *MemoryStore) Get(key string) (string, bool, error) {
|
||||
func (s *MemoryStore) Get(id string) (*Paste, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
val, ok := s.data[key]
|
||||
return val, ok, nil
|
||||
p, ok := s.pastes[id]
|
||||
return p, ok, nil
|
||||
}
|
||||
|
||||
// Set adds a value to the store.
|
||||
func (s *MemoryStore) Set(key, value string) error {
|
||||
func (s *MemoryStore) GetIDByHash(hash string) (string, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
id, ok := s.hashes[hash]
|
||||
return id, ok, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Set(id, hash, content string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.data[key] = value
|
||||
s.pastes[id] = &Paste{
|
||||
Content: content,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
s.hashes[hash] = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Del removes a value from the store.
|
||||
func (s *MemoryStore) Del(key string) error {
|
||||
func (s *MemoryStore) Del(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.data, key)
|
||||
delete(s.pastes, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
+45
-18
@@ -7,27 +7,54 @@ import (
|
||||
)
|
||||
|
||||
func TestMemoryStore(t *testing.T) {
|
||||
store := NewMemoryStore()
|
||||
s := NewMemoryStore()
|
||||
|
||||
// Test Set and Get
|
||||
err := store.Set("key1", "value1")
|
||||
assert.NoError(t, err)
|
||||
t.Run("Set and Get", func(t *testing.T) {
|
||||
id := "id1"
|
||||
hash := "hash1"
|
||||
content := "content1"
|
||||
|
||||
val, ok, err := store.Get("key1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "value1", val)
|
||||
err := s.Set(id, hash, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test Get non-existent key
|
||||
_, ok, err = store.Get("non-existent")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
p, exists, err := s.Get(id)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, content, p.Content)
|
||||
assert.NotZero(t, p.CreatedAt)
|
||||
})
|
||||
|
||||
// Test Delete
|
||||
err = store.Del("key1")
|
||||
assert.NoError(t, err)
|
||||
t.Run("GetIDByHash", func(t *testing.T) {
|
||||
id := "id2"
|
||||
hash := "hash2"
|
||||
content := "content2"
|
||||
|
||||
_, ok, err = store.Get("key1")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
s.Set(id, hash, content)
|
||||
storedID, exists, err := s.GetIDByHash(hash)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, id, storedID)
|
||||
})
|
||||
|
||||
t.Run("Get Non Existent", func(t *testing.T) {
|
||||
p, exists, err := s.Get("none")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, p)
|
||||
|
||||
id, exists, err := s.GetIDByHash("none")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
assert.Empty(t, id)
|
||||
})
|
||||
|
||||
t.Run("Del", func(t *testing.T) {
|
||||
id := "id3"
|
||||
s.Set(id, "h3", "c3")
|
||||
err := s.Del(id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, exists, _ := s.Get(id)
|
||||
assert.False(t, exists)
|
||||
})
|
||||
}
|
||||
|
||||
+12
-5
@@ -1,8 +1,15 @@
|
||||
package store
|
||||
|
||||
// Store is the interface for a key-value store.
|
||||
type Store interface {
|
||||
Get(key string) (string, bool, error)
|
||||
Set(key, value string) error
|
||||
Del(key string) error
|
||||
import "time"
|
||||
|
||||
type Paste struct {
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Get(id string) (*Paste, bool, error)
|
||||
GetIDByHash(hash string) (string, bool, error)
|
||||
Set(id, hash, content string) error
|
||||
Del(id string) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user