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 }