go-micro.dev/v5@v5.12.0/store/file.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	bolt "go.etcd.io/bbolt"
    14  )
    15  
    16  var (
    17  	HomeDir, _ = os.UserHomeDir()
    18  
    19  	// DefaultDatabase is the namespace that the bbolt store
    20  	// will use if no namespace is provided.
    21  	DefaultDatabase = "micro"
    22  	// DefaultTable when none is specified.
    23  	DefaultTable = "micro"
    24  	// DefaultDir is the default directory for bbolt files.
    25  	DefaultDir = filepath.Join(HomeDir, "micro", "store")
    26  
    27  	// bucket used for data storage.
    28  	dataBucket = "data"
    29  )
    30  
    31  func NewFileStore(opts ...Option) Store {
    32  	s := &fileStore{
    33  		handles: make(map[string]*fileHandle),
    34  	}
    35  	s.init(opts...)
    36  	return s
    37  }
    38  
    39  type fileStore struct {
    40  	options Options
    41  	dir     string
    42  
    43  	// the database handle
    44  	sync.RWMutex
    45  	handles map[string]*fileHandle
    46  }
    47  
    48  type fileHandle struct {
    49  	key string
    50  	db  *bolt.DB
    51  }
    52  
    53  // record stored by us.
    54  type record struct {
    55  	Key       string
    56  	Value     []byte
    57  	Metadata  map[string]interface{}
    58  	ExpiresAt time.Time
    59  }
    60  
    61  func key(database, table string) string {
    62  	return database + ":" + table
    63  }
    64  
    65  func (m *fileStore) delete(fd *fileHandle, key string) error {
    66  	return fd.db.Update(func(tx *bolt.Tx) error {
    67  		b := tx.Bucket([]byte(dataBucket))
    68  		if b == nil {
    69  			return nil
    70  		}
    71  		return b.Delete([]byte(key))
    72  	})
    73  }
    74  
    75  func (m *fileStore) init(opts ...Option) error {
    76  	for _, o := range opts {
    77  		o(&m.options)
    78  	}
    79  
    80  	if m.options.Database == "" {
    81  		m.options.Database = DefaultDatabase
    82  	}
    83  
    84  	if m.options.Table == "" {
    85  		// bbolt requires bucketname to not be empty
    86  		m.options.Table = DefaultTable
    87  	}
    88  
    89  	if m.options.Context != nil {
    90  		if dir, ok := m.options.Context.Value(dirOptionKey{}).(string); ok {
    91  			m.dir = dir
    92  		}
    93  	}
    94  
    95  	// create default directory
    96  	if m.dir == "" {
    97  		m.dir = DefaultDir
    98  	}
    99  	// create the directory
   100  	return os.MkdirAll(m.dir, 0700)
   101  }
   102  
   103  func (f *fileStore) getDB(database, table string) (*fileHandle, error) {
   104  	if len(database) == 0 {
   105  		database = f.options.Database
   106  	}
   107  	if len(table) == 0 {
   108  		table = f.options.Table
   109  	}
   110  
   111  	k := key(database, table)
   112  	f.RLock()
   113  	fd, ok := f.handles[k]
   114  	f.RUnlock()
   115  
   116  	// return the file handle
   117  	if ok {
   118  		return fd, nil
   119  	}
   120  
   121  	// double check locking
   122  	f.Lock()
   123  	defer f.Unlock()
   124  	if fd, ok := f.handles[k]; ok {
   125  		return fd, nil
   126  	}
   127  
   128  	// create directory
   129  	dir := filepath.Join(f.dir, database)
   130  	// create the database handle
   131  	fname := table + ".db"
   132  	// make the dir
   133  	os.MkdirAll(dir, 0700)
   134  	// database path
   135  	dbPath := filepath.Join(dir, fname)
   136  
   137  	// create new db handle
   138  	// Bolt DB only allows one process to open the file R/W so make sure we're doing this under a lock
   139  	db, err := bolt.Open(dbPath, 0700, &bolt.Options{Timeout: 5 * time.Second})
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	fd = &fileHandle{
   144  		key: k,
   145  		db:  db,
   146  	}
   147  	f.handles[k] = fd
   148  
   149  	return fd, nil
   150  }
   151  
   152  func (m *fileStore) list(fd *fileHandle, limit, offset uint) []string {
   153  	var allItems []string
   154  
   155  	fd.db.View(func(tx *bolt.Tx) error {
   156  		b := tx.Bucket([]byte(dataBucket))
   157  		// nothing to read
   158  		if b == nil {
   159  			return nil
   160  		}
   161  
   162  		// @todo very inefficient
   163  		if err := b.ForEach(func(k, v []byte) error {
   164  			storedRecord := &record{}
   165  
   166  			if err := json.Unmarshal(v, storedRecord); err != nil {
   167  				return err
   168  			}
   169  
   170  			if !storedRecord.ExpiresAt.IsZero() {
   171  				if storedRecord.ExpiresAt.Before(time.Now()) {
   172  					return nil
   173  				}
   174  			}
   175  
   176  			allItems = append(allItems, string(k))
   177  
   178  			return nil
   179  		}); err != nil {
   180  			return err
   181  		}
   182  
   183  		return nil
   184  	})
   185  
   186  	allKeys := make([]string, len(allItems))
   187  
   188  	for i, k := range allItems {
   189  		allKeys[i] = k
   190  	}
   191  
   192  	if limit != 0 || offset != 0 {
   193  		sort.Slice(allKeys, func(i, j int) bool { return allKeys[i] < allKeys[j] })
   194  		min := func(i, j uint) uint {
   195  			if i < j {
   196  				return i
   197  			}
   198  			return j
   199  		}
   200  		return allKeys[offset:min(limit, uint(len(allKeys)))]
   201  	}
   202  
   203  	return allKeys
   204  }
   205  
   206  func (m *fileStore) get(fd *fileHandle, k string) (*Record, error) {
   207  	var value []byte
   208  
   209  	fd.db.View(func(tx *bolt.Tx) error {
   210  		// @todo this is still very experimental...
   211  		b := tx.Bucket([]byte(dataBucket))
   212  		if b == nil {
   213  			return nil
   214  		}
   215  
   216  		value = b.Get([]byte(k))
   217  		return nil
   218  	})
   219  
   220  	if value == nil {
   221  		return nil, ErrNotFound
   222  	}
   223  
   224  	storedRecord := &record{}
   225  
   226  	if err := json.Unmarshal(value, storedRecord); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	newRecord := &Record{}
   231  	newRecord.Key = storedRecord.Key
   232  	newRecord.Value = storedRecord.Value
   233  	newRecord.Metadata = make(map[string]interface{})
   234  
   235  	for k, v := range storedRecord.Metadata {
   236  		newRecord.Metadata[k] = v
   237  	}
   238  
   239  	if !storedRecord.ExpiresAt.IsZero() {
   240  		if storedRecord.ExpiresAt.Before(time.Now()) {
   241  			return nil, ErrNotFound
   242  		}
   243  		newRecord.Expiry = time.Until(storedRecord.ExpiresAt)
   244  	}
   245  
   246  	return newRecord, nil
   247  }
   248  
   249  func (m *fileStore) set(fd *fileHandle, r *Record) error {
   250  	// copy the incoming record and then
   251  	// convert the expiry in to a hard timestamp
   252  	item := &record{}
   253  	item.Key = r.Key
   254  	item.Value = r.Value
   255  	item.Metadata = make(map[string]interface{})
   256  
   257  	if r.Expiry != 0 {
   258  		item.ExpiresAt = time.Now().Add(r.Expiry)
   259  	}
   260  
   261  	for k, v := range r.Metadata {
   262  		item.Metadata[k] = v
   263  	}
   264  
   265  	// marshal the data
   266  	data, _ := json.Marshal(item)
   267  
   268  	return fd.db.Update(func(tx *bolt.Tx) error {
   269  		b := tx.Bucket([]byte(dataBucket))
   270  		if b == nil {
   271  			var err error
   272  			b, err = tx.CreateBucketIfNotExists([]byte(dataBucket))
   273  			if err != nil {
   274  				return err
   275  			}
   276  		}
   277  		return b.Put([]byte(r.Key), data)
   278  	})
   279  }
   280  
   281  func (f *fileStore) Close() error {
   282  	f.Lock()
   283  	defer f.Unlock()
   284  	for k, v := range f.handles {
   285  		v.db.Close()
   286  		delete(f.handles, k)
   287  	}
   288  	return nil
   289  }
   290  
   291  func (f *fileStore) Init(opts ...Option) error {
   292  	return f.init(opts...)
   293  }
   294  
   295  func (m *fileStore) Delete(key string, opts ...DeleteOption) error {
   296  	var deleteOptions DeleteOptions
   297  	for _, o := range opts {
   298  		o(&deleteOptions)
   299  	}
   300  
   301  	fd, err := m.getDB(deleteOptions.Database, deleteOptions.Table)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	return m.delete(fd, key)
   307  }
   308  
   309  func (m *fileStore) Read(key string, opts ...ReadOption) ([]*Record, error) {
   310  	var readOpts ReadOptions
   311  	for _, o := range opts {
   312  		o(&readOpts)
   313  	}
   314  
   315  	fd, err := m.getDB(readOpts.Database, readOpts.Table)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  
   320  	var keys []string
   321  
   322  	// Handle Prefix / suffix
   323  	// TODO: do range scan here rather than listing all keys
   324  	if readOpts.Prefix || readOpts.Suffix {
   325  		// list the keys
   326  		k := m.list(fd, readOpts.Limit, readOpts.Offset)
   327  
   328  		// check for prefix and suffix
   329  		for _, v := range k {
   330  			if readOpts.Prefix && !strings.HasPrefix(v, key) {
   331  				continue
   332  			}
   333  			if readOpts.Suffix && !strings.HasSuffix(v, key) {
   334  				continue
   335  			}
   336  			keys = append(keys, v)
   337  		}
   338  	} else {
   339  		keys = []string{key}
   340  	}
   341  
   342  	var results []*Record
   343  
   344  	for _, k := range keys {
   345  		r, err := m.get(fd, k)
   346  		if err != nil {
   347  			return results, err
   348  		}
   349  		results = append(results, r)
   350  	}
   351  
   352  	return results, nil
   353  }
   354  
   355  func (m *fileStore) Write(r *Record, opts ...WriteOption) error {
   356  	var writeOpts WriteOptions
   357  	for _, o := range opts {
   358  		o(&writeOpts)
   359  	}
   360  
   361  	fd, err := m.getDB(writeOpts.Database, writeOpts.Table)
   362  	if err != nil {
   363  		return err
   364  	}
   365  
   366  	if len(opts) > 0 {
   367  		// Copy the record before applying options, or the incoming record will be mutated
   368  		newRecord := Record{}
   369  		newRecord.Key = r.Key
   370  		newRecord.Value = r.Value
   371  		newRecord.Metadata = make(map[string]interface{})
   372  		newRecord.Expiry = r.Expiry
   373  
   374  		if !writeOpts.Expiry.IsZero() {
   375  			newRecord.Expiry = time.Until(writeOpts.Expiry)
   376  		}
   377  		if writeOpts.TTL != 0 {
   378  			newRecord.Expiry = writeOpts.TTL
   379  		}
   380  
   381  		for k, v := range r.Metadata {
   382  			newRecord.Metadata[k] = v
   383  		}
   384  
   385  		return m.set(fd, &newRecord)
   386  	}
   387  
   388  	return m.set(fd, r)
   389  }
   390  
   391  func (m *fileStore) Options() Options {
   392  	return m.options
   393  }
   394  
   395  func (m *fileStore) List(opts ...ListOption) ([]string, error) {
   396  	var listOptions ListOptions
   397  
   398  	for _, o := range opts {
   399  		o(&listOptions)
   400  	}
   401  
   402  	fd, err := m.getDB(listOptions.Database, listOptions.Table)
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	// TODO apply prefix/suffix in range query
   408  	allKeys := m.list(fd, listOptions.Limit, listOptions.Offset)
   409  
   410  	if len(listOptions.Prefix) > 0 {
   411  		var prefixKeys []string
   412  		for _, k := range allKeys {
   413  			if strings.HasPrefix(k, listOptions.Prefix) {
   414  				prefixKeys = append(prefixKeys, k)
   415  			}
   416  		}
   417  		allKeys = prefixKeys
   418  	}
   419  
   420  	if len(listOptions.Suffix) > 0 {
   421  		var suffixKeys []string
   422  		for _, k := range allKeys {
   423  			if strings.HasSuffix(k, listOptions.Suffix) {
   424  				suffixKeys = append(suffixKeys, k)
   425  			}
   426  		}
   427  		allKeys = suffixKeys
   428  	}
   429  
   430  	return allKeys, nil
   431  }
   432  
   433  func (m *fileStore) String() string {
   434  	return "file"
   435  }
   436  
   437  type dirOptionKey struct{}
   438  
   439  // DirOption is a file store Option to set the directory for the file
   440  func DirOption(dir string) Option {
   441  	return func(o *Options) {
   442  		if o.Context == nil {
   443  			o.Context = context.Background()
   444  		}
   445  		o.Context = context.WithValue(o.Context, dirOptionKey{}, dir)
   446  	}
   447  }