github.com/annwntech/go-micro/v2@v2.9.5/store/file/file.go (about)

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