github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/store/file/file.go (about)

     1  // Licensed under the Apache License, Version 2.0 (the "License");
     2  // you may not use this file except in compliance with the License.
     3  // You may obtain a copy of the License at
     4  //
     5  //     https://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS,
     9  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  // See the License for the specific language governing permissions and
    11  // limitations under the License.
    12  //
    13  // Original source: github.com/micro/go-micro/v3/store/file/file.go
    14  
    15  // Package local is a file system backed store
    16  package file
    17  
    18  import (
    19  	"bytes"
    20  	"encoding/json"
    21  	"os"
    22  	"path/filepath"
    23  	"time"
    24  
    25  	"github.com/tickoalcantara12/micro/v3/service/store"
    26  	bolt "go.etcd.io/bbolt"
    27  )
    28  
    29  var (
    30  	// DefaultDatabase is the namespace that the bbolt store
    31  	// will use if no namespace is provided.
    32  	DefaultDatabase = "micro"
    33  	// DefaultTable when none is specified
    34  	DefaultTable = "micro"
    35  	// DefaultDir is the default directory for bbolt files
    36  	DefaultDir = filepath.Join(os.TempDir(), "micro", "store")
    37  
    38  	// bucket used for data storage
    39  	dataBucket = "data"
    40  )
    41  
    42  // NewStore returns a file store
    43  func NewStore(opts ...store.Option) store.Store {
    44  	s := &fileStore{}
    45  	s.init(opts...)
    46  	return s
    47  }
    48  
    49  type fileStore struct {
    50  	options store.Options
    51  	dir     string
    52  }
    53  
    54  type fileHandle struct {
    55  	key string
    56  	db  *bolt.DB
    57  }
    58  
    59  // record stored by us
    60  type record struct {
    61  	Key       string
    62  	Value     []byte
    63  	Metadata  map[string]interface{}
    64  	ExpiresAt time.Time
    65  }
    66  
    67  func key(database, table string) string {
    68  	return database + ":" + table
    69  }
    70  
    71  func (m *fileStore) delete(db *bolt.DB, key string) error {
    72  	return db.Update(func(tx *bolt.Tx) error {
    73  		b := tx.Bucket([]byte(dataBucket))
    74  		if b == nil {
    75  			return nil
    76  		}
    77  		return b.Delete([]byte(key))
    78  	})
    79  }
    80  
    81  func (m *fileStore) init(opts ...store.Option) error {
    82  	for _, o := range opts {
    83  		o(&m.options)
    84  	}
    85  
    86  	if m.options.Database == "" {
    87  		m.options.Database = DefaultDatabase
    88  	}
    89  
    90  	if m.options.Table == "" {
    91  		// bbolt requires bucketname to not be empty
    92  		m.options.Table = DefaultTable
    93  	}
    94  
    95  	// Ignoring this as the folder might exist.
    96  	// Reads/Writes updates will return with sensible error messages
    97  	// about the dir not existing in case this cannot create the path anyway
    98  	dir := m.getDir(m.options.Database)
    99  	os.MkdirAll(dir, 0700)
   100  	return nil
   101  }
   102  
   103  // getDir returns the directory which should contain the files for a databases
   104  func (m *fileStore) getDir(db string) string {
   105  	// get the directory option from the context
   106  	var directory string
   107  	if m.options.Context != nil {
   108  		fd, ok := m.options.Context.Value(dirKey{}).(string)
   109  		if ok {
   110  			directory = fd
   111  		}
   112  	}
   113  	if len(directory) == 0 {
   114  		directory = DefaultDir
   115  	}
   116  
   117  	// construct the directory, e.g. /tmp/micro
   118  	return filepath.Join(directory, db)
   119  }
   120  
   121  func (f *fileStore) getDB(database, table string) (*bolt.DB, error) {
   122  	if len(database) == 0 {
   123  		database = f.options.Database
   124  	}
   125  	if len(table) == 0 {
   126  		table = f.options.Table
   127  	}
   128  
   129  	// create a directory /tmp/micro
   130  	dir := f.getDir(database)
   131  	// create the database handle
   132  	fname := table + ".db"
   133  	// make the dir
   134  	os.MkdirAll(dir, 0700)
   135  	// database path
   136  	dbPath := filepath.Join(dir, fname)
   137  
   138  	// create new db handle
   139  	// Bolt DB only allows one process to open the file R/W so make sure we're doing this under a lock
   140  	return bolt.Open(dbPath, 0700, &bolt.Options{Timeout: 5 * time.Second})
   141  }
   142  
   143  func (m *fileStore) list(db *bolt.DB, order store.Order, limit, offset uint, prefix, suffix string) []string {
   144  	var keys []string
   145  
   146  	db.View(func(tx *bolt.Tx) error {
   147  		b := tx.Bucket([]byte(dataBucket))
   148  		// nothing to read
   149  		if b == nil {
   150  			return nil
   151  		}
   152  		c := b.Cursor()
   153  		var k, v []byte
   154  		var cont func(k []byte) bool
   155  
   156  		if prefix != "" {
   157  			// for prefix we can speed up the search, not for suffix though :(
   158  			k, v = c.Seek([]byte(prefix))
   159  			cont = func(k []byte) bool {
   160  				return bytes.HasPrefix(k, []byte(prefix))
   161  			}
   162  		} else {
   163  			k, v = c.First()
   164  			cont = func(k []byte) bool {
   165  				return true
   166  			}
   167  		}
   168  
   169  		// get all the key/vals
   170  		for ; k != nil && cont(k); k, v = c.Next() {
   171  			storedRecord := &record{}
   172  
   173  			if err := json.Unmarshal(v, storedRecord); err != nil {
   174  				return err
   175  			}
   176  			if !storedRecord.ExpiresAt.IsZero() {
   177  				if storedRecord.ExpiresAt.Before(time.Now()) {
   178  					continue
   179  				}
   180  			}
   181  			if suffix != "" && !bytes.HasSuffix(k, []byte(suffix)) {
   182  				continue
   183  			}
   184  
   185  			keys = append(keys, string(k))
   186  		}
   187  
   188  		return nil
   189  	})
   190  
   191  	// now check if we need do ordering
   192  	if order == store.OrderDesc {
   193  		for i, j := 0, len(keys)-1; i < j; i, j = i+1, j-1 {
   194  			keys[i], keys[j] = keys[j], keys[i]
   195  		}
   196  	}
   197  
   198  	// build a new key set
   199  	var keyList []string
   200  
   201  	if offset > 0 {
   202  		// offset is greater than the keys we have
   203  		if int(offset) >= len(keys) {
   204  			return nil
   205  		}
   206  
   207  		// otherwise set the offset for the keys
   208  		keys = keys[offset:]
   209  	}
   210  
   211  	// check key limit
   212  	if v := int(limit); v == 0 || v > len(keys) {
   213  		limit = uint(len(keys))
   214  	}
   215  
   216  	for i := 0; i < int(limit); i++ {
   217  		keyList = append(keyList, keys[i])
   218  	}
   219  
   220  	return keyList
   221  }
   222  
   223  func (m *fileStore) get(db *bolt.DB, k string) (*store.Record, error) {
   224  	var value []byte
   225  
   226  	db.View(func(tx *bolt.Tx) error {
   227  		// @todo this is still very experimental...
   228  		b := tx.Bucket([]byte(dataBucket))
   229  		if b == nil {
   230  			return nil
   231  		}
   232  
   233  		value = b.Get([]byte(k))
   234  		return nil
   235  	})
   236  
   237  	if value == nil {
   238  		return nil, store.ErrNotFound
   239  	}
   240  
   241  	storedRecord := &record{}
   242  
   243  	if err := json.Unmarshal(value, storedRecord); err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	newRecord := &store.Record{}
   248  	newRecord.Key = storedRecord.Key
   249  	newRecord.Value = storedRecord.Value
   250  	newRecord.Metadata = make(map[string]interface{})
   251  
   252  	for k, v := range storedRecord.Metadata {
   253  		newRecord.Metadata[k] = v
   254  	}
   255  
   256  	if !storedRecord.ExpiresAt.IsZero() {
   257  		if storedRecord.ExpiresAt.Before(time.Now()) {
   258  			return nil, store.ErrNotFound
   259  		}
   260  		newRecord.Expiry = time.Until(storedRecord.ExpiresAt)
   261  	}
   262  
   263  	return newRecord, nil
   264  }
   265  
   266  func (m *fileStore) set(db *bolt.DB, r *store.Record) error {
   267  	// copy the incoming record and then
   268  	// convert the expiry in to a hard timestamp
   269  	item := &record{}
   270  	item.Key = r.Key
   271  	item.Value = r.Value
   272  	item.Metadata = make(map[string]interface{})
   273  
   274  	if r.Expiry != 0 {
   275  		item.ExpiresAt = time.Now().Add(r.Expiry)
   276  	}
   277  
   278  	for k, v := range r.Metadata {
   279  		item.Metadata[k] = v
   280  	}
   281  
   282  	// marshal the data
   283  	data, _ := json.Marshal(item)
   284  
   285  	return db.Update(func(tx *bolt.Tx) error {
   286  		b := tx.Bucket([]byte(dataBucket))
   287  		if b == nil {
   288  			var err error
   289  			b, err = tx.CreateBucketIfNotExists([]byte(dataBucket))
   290  			if err != nil {
   291  				return err
   292  			}
   293  		}
   294  		return b.Put([]byte(r.Key), data)
   295  	})
   296  }
   297  
   298  func (f *fileStore) Close() error {
   299  	return nil
   300  }
   301  
   302  func (f *fileStore) Init(opts ...store.Option) error {
   303  	return f.init(opts...)
   304  }
   305  
   306  func (m *fileStore) Delete(key string, opts ...store.DeleteOption) error {
   307  	var deleteOptions store.DeleteOptions
   308  	for _, o := range opts {
   309  		o(&deleteOptions)
   310  	}
   311  
   312  	db, err := m.getDB(deleteOptions.Database, deleteOptions.Table)
   313  	if err != nil {
   314  		return err
   315  	}
   316  	defer db.Close()
   317  
   318  	return m.delete(db, key)
   319  }
   320  
   321  func (m *fileStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
   322  	var readOpts store.ReadOptions
   323  	for _, o := range opts {
   324  		o(&readOpts)
   325  	}
   326  
   327  	db, err := m.getDB(readOpts.Database, readOpts.Table)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	defer db.Close()
   332  
   333  	var keys []string
   334  
   335  	// Handle Prefix / suffix
   336  	if readOpts.Prefix || readOpts.Suffix {
   337  		prefix := ""
   338  		if readOpts.Prefix {
   339  			prefix = key
   340  		}
   341  		suffix := ""
   342  		if readOpts.Suffix {
   343  			suffix = key
   344  		}
   345  		// list the keys
   346  		keys = m.list(db, readOpts.Order, readOpts.Limit, readOpts.Offset, prefix, suffix)
   347  	} else {
   348  		keys = []string{key}
   349  	}
   350  
   351  	var results []*store.Record
   352  
   353  	for _, k := range keys {
   354  		r, err := m.get(db, k)
   355  		if err != nil {
   356  			return results, err
   357  		}
   358  		results = append(results, r)
   359  	}
   360  
   361  	return results, nil
   362  }
   363  
   364  func (m *fileStore) Write(r *store.Record, opts ...store.WriteOption) error {
   365  	var writeOpts store.WriteOptions
   366  	for _, o := range opts {
   367  		o(&writeOpts)
   368  	}
   369  
   370  	db, err := m.getDB(writeOpts.Database, writeOpts.Table)
   371  	if err != nil {
   372  		return err
   373  	}
   374  	defer db.Close()
   375  
   376  	if len(opts) > 0 {
   377  		// Copy the record before applying options, or the incoming record will be mutated
   378  		newRecord := store.Record{}
   379  		newRecord.Key = r.Key
   380  		newRecord.Value = r.Value
   381  		newRecord.Metadata = make(map[string]interface{})
   382  		newRecord.Expiry = r.Expiry
   383  
   384  		for k, v := range r.Metadata {
   385  			newRecord.Metadata[k] = v
   386  		}
   387  
   388  		return m.set(db, &newRecord)
   389  	}
   390  
   391  	return m.set(db, r)
   392  }
   393  
   394  func (m *fileStore) Options() store.Options {
   395  	return m.options
   396  }
   397  
   398  func (m *fileStore) List(opts ...store.ListOption) ([]string, error) {
   399  	var listOptions store.ListOptions
   400  
   401  	for _, o := range opts {
   402  		o(&listOptions)
   403  	}
   404  
   405  	db, err := m.getDB(listOptions.Database, listOptions.Table)
   406  	if err != nil {
   407  		return nil, err
   408  	}
   409  	defer db.Close()
   410  
   411  	allKeys := m.list(db, listOptions.Order, listOptions.Limit, listOptions.Offset, listOptions.Prefix, listOptions.Suffix)
   412  
   413  	return allKeys, nil
   414  }
   415  
   416  func (m *fileStore) String() string {
   417  	return "file"
   418  }