kanban

Kanban client in Gio
Log | Files | Refs | README | LICENSE

bolt.go (4958B)


      1 package bolt
      2 
      3 import (
      4 	"encoding/json"
      5 	"fmt"
      6 
      7 	"git.sr.ht/~jackmordaunt/kanban"
      8 	"git.sr.ht/~jackmordaunt/kanban/storage"
      9 	"github.com/boltdb/bolt"
     10 	"github.com/google/uuid"
     11 )
     12 
     13 var _ storage.Storer = (*Storer)(nil)
     14 
     15 type Storer struct {
     16 	*bolt.DB
     17 }
     18 
     19 type Bucket []byte
     20 
     21 func (b Bucket) String() string {
     22 	return string(b)
     23 }
     24 
     25 var (
     26 	BucketProject Bucket = Bucket("Project")
     27 	BucketArchive Bucket = Bucket("Archive")
     28 )
     29 
     30 func Open(path string) (*Storer, error) {
     31 	db, err := bolt.Open(path, 0660, nil)
     32 	if err != nil {
     33 		return nil, fmt.Errorf("opening database file: %w", err)
     34 	}
     35 	if err := db.Update(func(tx *bolt.Tx) error {
     36 		if _, err := tx.CreateBucketIfNotExists(BucketProject); err != nil {
     37 			return err
     38 		}
     39 		if _, err := tx.CreateBucketIfNotExists(BucketArchive); err != nil {
     40 			return err
     41 		}
     42 		return nil
     43 	}); err != nil {
     44 		return nil, fmt.Errorf("initializing buckets: %w", err)
     45 	}
     46 	return &Storer{DB: db}, nil
     47 }
     48 
     49 func (db *Storer) Create(p kanban.Project) error {
     50 	id, err := p.ID.MarshalBinary()
     51 	if err != nil {
     52 		return fmt.Errorf("serializing project ID: %w", err)
     53 	}
     54 	v, err := json.Marshal(p)
     55 	if err != nil {
     56 		return fmt.Errorf("serializing project: %w", err)
     57 	}
     58 	return db.Update(func(tx *bolt.Tx) error {
     59 		b := tx.Bucket(BucketProject)
     60 		if b == nil {
     61 			return fmt.Errorf("bucket not initialized: %s", BucketProject)
     62 		}
     63 		if b.Get(id) != nil {
     64 			return fmt.Errorf("project already exists for ID %q", p.ID)
     65 		}
     66 		return b.Put(id, v)
     67 	})
     68 }
     69 
     70 // Save persists the provided projects.
     71 // If a project is nil, or doesn't exist in the store, it will be zeroed out
     72 // in the slice.
     73 // The caller can then cleanup the slice by removing the zeroed out projects.
     74 func (db *Storer) Save(projects ...kanban.Project) error {
     75 	return db.Update(func(tx *bolt.Tx) error {
     76 		for ii, p := range projects {
     77 			if p.ID == uuid.Nil {
     78 				continue
     79 			}
     80 			id, err := p.ID.MarshalBinary()
     81 			if err != nil {
     82 				return fmt.Errorf("serializing project ID: %w", err)
     83 			}
     84 			v, err := json.Marshal(p)
     85 			if err != nil {
     86 				return fmt.Errorf("serializing project: %w", err)
     87 			}
     88 			if b := tx.Bucket(BucketProject); b != nil {
     89 				if b.Get(id) == nil {
     90 					projects[ii] = kanban.Project{}
     91 					continue
     92 				}
     93 				if err := b.Put(id, v); err != nil {
     94 					return fmt.Errorf("updating project: %w", err)
     95 				}
     96 			}
     97 		}
     98 		return nil
     99 	})
    100 }
    101 
    102 func (db *Storer) Find(id uuid.UUID) (p kanban.Project, ok bool, err error) {
    103 	key, err := id.MarshalBinary()
    104 	if err != nil {
    105 		return p, false, fmt.Errorf("serializing id: %w", err)
    106 	}
    107 	return p, ok, db.View(func(tx *bolt.Tx) error {
    108 		if err := json.Unmarshal(tx.Bucket(BucketProject).Get(key), &p); err != nil {
    109 			return fmt.Errorf("deserializing project: %w", err)
    110 		}
    111 		ok = true
    112 		return nil
    113 	})
    114 }
    115 
    116 func (db *Storer) List() ([]kanban.Project, error) {
    117 	return db.list(BucketProject)
    118 }
    119 
    120 func (db *Storer) Load(projects []kanban.Project) error {
    121 	if len(projects) > 0 {
    122 		if projects[0].ID == uuid.Nil {
    123 			list, err := db.List()
    124 			if err != nil {
    125 				return err
    126 			}
    127 			copy(projects, list)
    128 			return nil
    129 		}
    130 	}
    131 	return db.View(func(tx *bolt.Tx) error {
    132 		for ii, p := range projects {
    133 			id, err := p.ID.MarshalBinary()
    134 			if err != nil {
    135 				return fmt.Errorf("serializing project ID: %w", err)
    136 			}
    137 			if err := json.Unmarshal(tx.Bucket(BucketProject).Get(id), &projects[ii]); err != nil {
    138 				return fmt.Errorf("deserializing project: %w", err)
    139 			}
    140 		}
    141 		return nil
    142 	})
    143 }
    144 
    145 func (db *Storer) Count() (count int, err error) {
    146 	return count, db.View(func(tx *bolt.Tx) error {
    147 		c := tx.Bucket(BucketProject).Cursor()
    148 		for k, _ := c.First(); k != nil; k, _ = c.Next() {
    149 			count++
    150 		}
    151 		return nil
    152 	})
    153 }
    154 
    155 func (db *Storer) Archive(id uuid.UUID) error {
    156 	return db.move(id, BucketProject, BucketArchive)
    157 }
    158 
    159 func (db *Storer) Restore(id uuid.UUID) error {
    160 	return db.move(id, BucketArchive, BucketProject)
    161 }
    162 
    163 func (db *Storer) ListArchived() ([]kanban.Project, error) {
    164 	return db.list(BucketArchive)
    165 }
    166 
    167 func (db *Storer) move(id uuid.UUID, from, to Bucket) error {
    168 	k, err := id.MarshalBinary()
    169 	if err != nil {
    170 		return fmt.Errorf("serializing ID: %w", err)
    171 	}
    172 	return db.Update(func(tx *bolt.Tx) error {
    173 		fromBucket := tx.Bucket(from)
    174 		v := fromBucket.Get(k)
    175 		if v == nil {
    176 			return fmt.Errorf("project does not exist in %q bucket: %q", from, id)
    177 		}
    178 		if err := fromBucket.Delete(k); err != nil {
    179 			return fmt.Errorf("deleting project from %q bucket: %w", from, err)
    180 		}
    181 		if err := tx.Bucket(to).Put(k, v); err != nil {
    182 			return fmt.Errorf("placing project in %q bucket: %w", to, err)
    183 		}
    184 		return nil
    185 	})
    186 }
    187 
    188 func (db *Storer) list(from Bucket) (list []kanban.Project, err error) {
    189 	return list, db.View(func(tx *bolt.Tx) error {
    190 		c := tx.Bucket(from).Cursor()
    191 		for k, v := c.First(); k != nil; k, v = c.Next() {
    192 			var p kanban.Project
    193 			if err := json.Unmarshal(v, &p); err != nil {
    194 				return fmt.Errorf("deserializing project: %w", err)
    195 			}
    196 			list = append(list, p)
    197 		}
    198 		return nil
    199 	})
    200 }