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 }