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 }