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  }