github.com/safing/portbase@v0.19.5/database/storage/bbolt/bbolt.go (about) 1 package bbolt 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "path/filepath" 9 "time" 10 11 "go.etcd.io/bbolt" 12 13 "github.com/safing/portbase/database/iterator" 14 "github.com/safing/portbase/database/query" 15 "github.com/safing/portbase/database/record" 16 "github.com/safing/portbase/database/storage" 17 ) 18 19 var bucketName = []byte{0} 20 21 // BBolt database made pluggable for portbase. 22 type BBolt struct { 23 name string 24 db *bbolt.DB 25 } 26 27 func init() { 28 _ = storage.Register("bbolt", NewBBolt) 29 } 30 31 // NewBBolt opens/creates a bbolt database. 32 func NewBBolt(name, location string) (storage.Interface, error) { 33 // Create options for bbolt database. 34 dbFile := filepath.Join(location, "db.bbolt") 35 dbOptions := &bbolt.Options{ 36 Timeout: 1 * time.Second, 37 } 38 39 // Open/Create database, retry if there is a timeout. 40 db, err := bbolt.Open(dbFile, 0o0600, dbOptions) 41 for i := 0; i < 5 && err != nil; i++ { 42 // Try again if there is an error. 43 db, err = bbolt.Open(dbFile, 0o0600, dbOptions) 44 } 45 if err != nil { 46 return nil, err 47 } 48 49 // Create bucket 50 err = db.Update(func(tx *bbolt.Tx) error { 51 _, err := tx.CreateBucketIfNotExists(bucketName) 52 if err != nil { 53 return err 54 } 55 return nil 56 }) 57 if err != nil { 58 return nil, err 59 } 60 61 return &BBolt{ 62 name: name, 63 db: db, 64 }, nil 65 } 66 67 // Get returns a database record. 68 func (b *BBolt) Get(key string) (record.Record, error) { 69 var r record.Record 70 71 err := b.db.View(func(tx *bbolt.Tx) error { 72 // get value from db 73 value := tx.Bucket(bucketName).Get([]byte(key)) 74 if value == nil { 75 return storage.ErrNotFound 76 } 77 78 // copy data 79 duplicate := make([]byte, len(value)) 80 copy(duplicate, value) 81 82 // create record 83 var txErr error 84 r, txErr = record.NewRawWrapper(b.name, key, duplicate) 85 if txErr != nil { 86 return txErr 87 } 88 return nil 89 }) 90 if err != nil { 91 return nil, err 92 } 93 return r, nil 94 } 95 96 // GetMeta returns the metadata of a database record. 97 func (b *BBolt) GetMeta(key string) (*record.Meta, error) { 98 // TODO: Replace with more performant variant. 99 100 r, err := b.Get(key) 101 if err != nil { 102 return nil, err 103 } 104 105 return r.Meta(), nil 106 } 107 108 // Put stores a record in the database. 109 func (b *BBolt) Put(r record.Record) (record.Record, error) { 110 data, err := r.MarshalRecord(r) 111 if err != nil { 112 return nil, err 113 } 114 115 err = b.db.Update(func(tx *bbolt.Tx) error { 116 txErr := tx.Bucket(bucketName).Put([]byte(r.DatabaseKey()), data) 117 if txErr != nil { 118 return txErr 119 } 120 return nil 121 }) 122 if err != nil { 123 return nil, err 124 } 125 return r, nil 126 } 127 128 // PutMany stores many records in the database. 129 func (b *BBolt) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error) { 130 batch := make(chan record.Record, 100) 131 errs := make(chan error, 1) 132 133 go func() { 134 err := b.db.Batch(func(tx *bbolt.Tx) error { 135 bucket := tx.Bucket(bucketName) 136 for r := range batch { 137 txErr := b.batchPutOrDelete(bucket, shadowDelete, r) 138 if txErr != nil { 139 return txErr 140 } 141 } 142 return nil 143 }) 144 errs <- err 145 }() 146 147 return batch, errs 148 } 149 150 func (b *BBolt) batchPutOrDelete(bucket *bbolt.Bucket, shadowDelete bool, r record.Record) (err error) { 151 r.Lock() 152 defer r.Unlock() 153 154 if !shadowDelete && r.Meta().IsDeleted() { 155 // Immediate delete. 156 err = bucket.Delete([]byte(r.DatabaseKey())) 157 } else { 158 // Put or shadow delete. 159 var data []byte 160 data, err = r.MarshalRecord(r) 161 if err == nil { 162 err = bucket.Put([]byte(r.DatabaseKey()), data) 163 } 164 } 165 166 return err 167 } 168 169 // Delete deletes a record from the database. 170 func (b *BBolt) Delete(key string) error { 171 err := b.db.Update(func(tx *bbolt.Tx) error { 172 txErr := tx.Bucket(bucketName).Delete([]byte(key)) 173 if txErr != nil { 174 return txErr 175 } 176 return nil 177 }) 178 if err != nil { 179 return err 180 } 181 return nil 182 } 183 184 // Query returns a an iterator for the supplied query. 185 func (b *BBolt) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) { 186 _, err := q.Check() 187 if err != nil { 188 return nil, fmt.Errorf("invalid query: %w", err) 189 } 190 191 queryIter := iterator.New() 192 193 go b.queryExecutor(queryIter, q, local, internal) 194 return queryIter, nil 195 } 196 197 func (b *BBolt) queryExecutor(queryIter *iterator.Iterator, q *query.Query, local, internal bool) { 198 prefix := []byte(q.DatabaseKeyPrefix()) 199 err := b.db.View(func(tx *bbolt.Tx) error { 200 // Create a cursor for iteration. 201 c := tx.Bucket(bucketName).Cursor() 202 203 // Iterate over items in sorted key order. This starts from the 204 // first key/value pair and updates the k/v variables to the 205 // next key/value on each iteration. 206 // 207 // The loop finishes at the end of the cursor when a nil key is returned. 208 for key, value := c.Seek(prefix); key != nil; key, value = c.Next() { 209 210 // if we don't match the prefix anymore, exit 211 if !bytes.HasPrefix(key, prefix) { 212 return nil 213 } 214 215 // wrap value 216 iterWrapper, err := record.NewRawWrapper(b.name, string(key), value) 217 if err != nil { 218 return err 219 } 220 221 // check validity / access 222 if !iterWrapper.Meta().CheckValidity() { 223 continue 224 } 225 if !iterWrapper.Meta().CheckPermission(local, internal) { 226 continue 227 } 228 229 // check if matches & send 230 if q.MatchesRecord(iterWrapper) { 231 // copy data 232 duplicate := make([]byte, len(value)) 233 copy(duplicate, value) 234 235 newWrapper, err := record.NewRawWrapper(b.name, iterWrapper.DatabaseKey(), duplicate) 236 if err != nil { 237 return err 238 } 239 select { 240 case <-queryIter.Done: 241 return nil 242 case queryIter.Next <- newWrapper: 243 default: 244 select { 245 case <-queryIter.Done: 246 return nil 247 case queryIter.Next <- newWrapper: 248 case <-time.After(1 * time.Second): 249 return errors.New("query timeout") 250 } 251 } 252 } 253 } 254 return nil 255 }) 256 queryIter.Finish(err) 257 } 258 259 // ReadOnly returns whether the database is read only. 260 func (b *BBolt) ReadOnly() bool { 261 return false 262 } 263 264 // Injected returns whether the database is injected. 265 func (b *BBolt) Injected() bool { 266 return false 267 } 268 269 // MaintainRecordStates maintains records states in the database. 270 func (b *BBolt) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error { //nolint:gocognit 271 now := time.Now().Unix() 272 purgeThreshold := purgeDeletedBefore.Unix() 273 274 return b.db.Update(func(tx *bbolt.Tx) error { 275 bucket := tx.Bucket(bucketName) 276 // Create a cursor for iteration. 277 c := bucket.Cursor() 278 for key, value := c.First(); key != nil; key, value = c.Next() { 279 // check if context is cancelled 280 select { 281 case <-ctx.Done(): 282 return nil 283 default: 284 } 285 286 // wrap value 287 wrapper, err := record.NewRawWrapper(b.name, string(key), value) 288 if err != nil { 289 return err 290 } 291 292 // check if we need to do maintenance 293 meta := wrapper.Meta() 294 switch { 295 case meta.Deleted == 0 && meta.Expires > 0 && meta.Expires < now: 296 if shadowDelete { 297 // mark as deleted 298 meta.Deleted = meta.Expires 299 deleted, err := wrapper.MarshalRecord(wrapper) 300 if err != nil { 301 return err 302 } 303 err = bucket.Put(key, deleted) 304 if err != nil { 305 return err 306 } 307 308 // Cursor repositioning is required after modifying data. 309 // While the documentation states that this is also required after a 310 // delete, this actually makes the cursor skip a record with the 311 // following c.Next() call of the loop. 312 // Docs/Issue: https://github.com/boltdb/bolt/issues/426#issuecomment-141982984 313 c.Seek(key) 314 315 continue 316 } 317 318 // Immediately delete expired entries if shadowDelete is disabled. 319 fallthrough 320 case meta.Deleted > 0 && (!shadowDelete || meta.Deleted < purgeThreshold): 321 // delete from storage 322 err = c.Delete() 323 if err != nil { 324 return err 325 } 326 } 327 } 328 return nil 329 }) 330 } 331 332 // Purge deletes all records that match the given query. It returns the number of successful deletes and an error. 333 func (b *BBolt) Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) { //nolint:gocognit 334 prefix := []byte(q.DatabaseKeyPrefix()) 335 336 var cnt int 337 var done bool 338 for !done { 339 err := b.db.Update(func(tx *bbolt.Tx) error { 340 // Create a cursor for iteration. 341 bucket := tx.Bucket(bucketName) 342 c := bucket.Cursor() 343 for key, value := c.Seek(prefix); key != nil; key, value = c.Next() { 344 // Check if context has been cancelled. 345 select { 346 case <-ctx.Done(): 347 done = true 348 return nil 349 default: 350 } 351 352 // Check if we still match the key prefix, if not, exit. 353 if !bytes.HasPrefix(key, prefix) { 354 done = true 355 return nil 356 } 357 358 // Wrap the value in a new wrapper to access the metadata. 359 wrapper, err := record.NewRawWrapper(b.name, string(key), value) 360 if err != nil { 361 return err 362 } 363 364 // Check if we have permission for this record. 365 if !wrapper.Meta().CheckPermission(local, internal) { 366 continue 367 } 368 369 // Check if record is already deleted. 370 if wrapper.Meta().IsDeleted() { 371 continue 372 } 373 374 // Check if the query matches this record. 375 if !q.MatchesRecord(wrapper) { 376 continue 377 } 378 379 // Delete record. 380 if shadowDelete { 381 // Shadow delete. 382 wrapper.Meta().Delete() 383 deleted, err := wrapper.MarshalRecord(wrapper) 384 if err != nil { 385 return err 386 } 387 err = bucket.Put(key, deleted) 388 if err != nil { 389 return err 390 } 391 392 // Cursor repositioning is required after modifying data. 393 // While the documentation states that this is also required after a 394 // delete, this actually makes the cursor skip a record with the 395 // following c.Next() call of the loop. 396 // Docs/Issue: https://github.com/boltdb/bolt/issues/426#issuecomment-141982984 397 c.Seek(key) 398 399 } else { 400 // Immediate delete. 401 err = c.Delete() 402 if err != nil { 403 return err 404 } 405 } 406 407 // Work in batches of 1000 changes in order to enable other operations in between. 408 cnt++ 409 if cnt%1000 == 0 { 410 return nil 411 } 412 } 413 done = true 414 return nil 415 }) 416 if err != nil { 417 return cnt, err 418 } 419 } 420 421 return cnt, nil 422 } 423 424 // Shutdown shuts down the database. 425 func (b *BBolt) Shutdown() error { 426 return b.db.Close() 427 }