├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── snapshot.go └── snapshot_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | _sc_* 2 | *.out 3 | .idea 4 | *.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.4 7 | - go: 1.5 8 | - go: 1.6 9 | - go: 1.7 10 | - go: 1.8 11 | - go: 1.9 12 | - go: tip 13 | allow_failures: 14 | - go: tip 15 | 16 | script: 17 | - go get -t -v ./... 18 | - diff -u <(echo -n) <(gofmt -d .) 19 | - go vet $(go list ./... | grep -v /vendor/) 20 | - go test -v -race ./... 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Saddam H 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snapshot 2 | 3 | 4 | [![Build Status](https://travis-ci.org/thedevsaddam/snapshot.svg?branch=master)](https://travis-ci.org/thedevsaddam/snapshot) 5 | ![Project status](https://img.shields.io/badge/version-0.1-green.svg) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/thedevsaddam/snapshot)](https://goreportcard.com/report/github.com/thedevsaddam/snapshot) 7 | [![GoDoc](https://godoc.org/github.com/thedevsaddam/govalidator?status.svg)](https://godoc.org/github.com/thedevsaddam/snapshot) 8 | ![License](https://img.shields.io/dub/l/vibe-d.svg) 9 | 10 | Robust, Persistent, Key-Value (KV) store purely written in Golang 11 | 12 | ### Installation 13 | ```bash 14 | $ go get github.com/thedevsaddam/snapshot 15 | ``` 16 | 17 | ### Usage 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "github.com/thedevsaddam/snapshot" 25 | "time" 26 | ) 27 | 28 | //make a type to use 29 | type User struct { 30 | Name, Occupation string 31 | CreatedAt time.Time 32 | } 33 | 34 | func main() { 35 | 36 | //create a snapshot collection 37 | userCollection, err := snapshot.New("users") 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | 42 | //add item to collection 43 | userCollection.Put("john", User{Name: "John Doe", Occupation: "Software Engineer", CreatedAt: time.Now()}) 44 | userCollection.Put("jane", User{Name: "Jane Doe", Occupation: "UI/UX Designer", CreatedAt: time.Now()}) 45 | 46 | //get an item from collection 47 | john := User{} 48 | userCollection.Get("john", &john) 49 | fmt.Printf("%s is a %s\n", john.Name, john.Occupation) //John Doe is a Software Engineer 50 | 51 | //check an item is exist in a collection 52 | fmt.Println(userCollection.Has("john")) //true 53 | fmt.Println(userCollection.Has("tom")) //false 54 | 55 | //get all the item keys list 56 | fmt.Println(userCollection.List()) 57 | 58 | //get total item count 59 | fmt.Println(userCollection.TotalItem()) 60 | 61 | //remove a key from collection 62 | userCollection.Remove("john") 63 | 64 | //remove all the keys with collection 65 | userCollection.Flush() 66 | 67 | } 68 | 69 | ``` 70 | 71 | 72 | ### License 73 | The **snapshot** is a open-source software licensed under the [MIT License](LICENSE.md). 74 | -------------------------------------------------------------------------------- /snapshot.go: -------------------------------------------------------------------------------- 1 | package snapshot 2 | 3 | import ( 4 | "encoding/gob" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | const ( 15 | extension = ".gob" 16 | prefix = "_sc_" 17 | ) 18 | 19 | var mutexList = make(map[string]*sync.Mutex) 20 | 21 | // Collection describes a collection of key value pairs 22 | type Collection struct { 23 | mutex sync.Mutex 24 | dir string 25 | items []string 26 | } 27 | 28 | // New return a instance of collection 29 | func New(name string) (*Collection, error) { 30 | if len(name) <= 0 { 31 | return &Collection{}, errors.New("Collection name can not be empty!") 32 | } 33 | //make file path correct 34 | dir := prefix + filepath.Clean(name) 35 | collection := Collection{ 36 | dir: dir, 37 | } 38 | //create directory with collection name 39 | return &collection, os.MkdirAll(dir, 0755) 40 | } 41 | 42 | //Put store a new key with value in the collection 43 | func (c *Collection) Put(key string, value interface{}) error { 44 | if len(key) <= 0 { 45 | return errors.New("Key can not be empty!") 46 | } 47 | path := filepath.Join(c.dir, key+extension) 48 | m := c.getMutex(path) 49 | m.Lock() 50 | defer m.Unlock() 51 | file, err := os.Create(path) 52 | defer file.Close() 53 | if err == nil { 54 | encoder := gob.NewEncoder(file) 55 | encoder.Encode(value) 56 | } 57 | return err 58 | } 59 | 60 | //Get retrieve a value from collection by key 61 | func (c *Collection) Get(key string, value interface{}) error { 62 | if len(key) <= 0 { 63 | return errors.New("Key can not be empty!") 64 | } 65 | path := filepath.Join(c.dir, key+extension) 66 | m := c.getMutex(path) 67 | m.Lock() 68 | defer m.Unlock() 69 | if !c.Has(key) { 70 | return fmt.Errorf("Key %s does not exist!", key) 71 | } 72 | file, err := os.Open(path) 73 | defer file.Close() 74 | if err == nil { 75 | decoder := gob.NewDecoder(file) 76 | err = decoder.Decode(value) 77 | } 78 | return err 79 | } 80 | 81 | //Remove delete a key from collection 82 | func (c *Collection) Remove(key string) error { 83 | if len(key) <= 0 { 84 | return errors.New("Key can not be empty!") 85 | } 86 | path := filepath.Join(c.dir, key+extension) 87 | m := c.getMutex(path) 88 | m.Lock() 89 | defer m.Unlock() 90 | if c.Has(key) { 91 | return os.Remove(path) 92 | } 93 | return fmt.Errorf("Key %s does not exist!", key) 94 | } 95 | 96 | //Flush delete a collection with its value 97 | func (c *Collection) Flush() error { 98 | if _, err := os.Stat(c.dir); err == nil { 99 | os.RemoveAll(c.dir) 100 | return err 101 | } 102 | return nil 103 | } 104 | 105 | //Has check a key exist in the collection 106 | func (c *Collection) Has(key string) bool { 107 | if len(key) <= 0 { 108 | return false 109 | } 110 | path := filepath.Join(c.dir, key+extension) 111 | if _, err := os.Stat(path); !os.IsNotExist(err) { 112 | return true 113 | } 114 | return false 115 | } 116 | 117 | //List fetch all items key in collection 118 | func (c *Collection) List() ([]string, error) { 119 | var items []string 120 | files, err := ioutil.ReadDir(c.dir) 121 | if err != nil { 122 | return items, err 123 | } 124 | for _, f := range files { 125 | item := f.Name() 126 | item = strings.Trim(item, extension) 127 | items = append(items, item) 128 | } 129 | return items, err 130 | } 131 | 132 | //TotalItem return total item count 133 | func (c *Collection) TotalItem() int { 134 | list, _ := c.List() 135 | return len(list) 136 | } 137 | 138 | //populate a package level mutex list 139 | // with key of full path of an item 140 | func (c *Collection) getMutex(path string) *sync.Mutex { 141 | c.mutex.Lock() 142 | defer c.mutex.Unlock() 143 | m, ok := mutexList[path] 144 | if !ok { 145 | m = &sync.Mutex{} 146 | mutexList[path] = m 147 | } 148 | return m 149 | } 150 | -------------------------------------------------------------------------------- /snapshot_test.go: -------------------------------------------------------------------------------- 1 | package snapshot 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | type user struct { 9 | Name, Email string 10 | Phone int 11 | CreatedAt time.Time 12 | } 13 | 14 | var userCollection *Collection 15 | 16 | func TestMain(m *testing.M) { 17 | userCollection, _ = New("users") 18 | m.Run() 19 | } 20 | 21 | func TestCollection_Put(t *testing.T) { 22 | john := user{"John Doe", "john.doe@mail.com", 9898787, time.Now()} 23 | err := userCollection.Put("john", &john) 24 | if err != nil { 25 | t.Error("Failed to Put in collection!") 26 | } 27 | } 28 | 29 | func TestCollection_Get(t *testing.T) { 30 | john := user{} 31 | err := userCollection.Get("john", &john) 32 | if err != nil { 33 | t.Error("Failed to Get from collection!") 34 | } 35 | if john.Name != "John Doe" { 36 | t.Error("Failed to Get correct data") 37 | } 38 | } 39 | 40 | func TestCollection_Has(t *testing.T) { 41 | if !userCollection.Has("john") { 42 | t.Error("Failed to check using Has method!") 43 | } 44 | } 45 | 46 | func TestCollection_List(t *testing.T) { 47 | list, err := userCollection.List() 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | if len(list) != 1 { 52 | t.Error("Failed to get collection keys list!") 53 | } 54 | } 55 | 56 | func TestCollection_TotalItem(t *testing.T) { 57 | if userCollection.TotalItem() != 1 { 58 | t.Error("Failed to count total item number!") 59 | } 60 | } 61 | 62 | func TestCollection_Remove(t *testing.T) { 63 | err := userCollection.Remove("john") 64 | if err != nil { 65 | t.Error("Failed to remove from collection!") 66 | } 67 | } 68 | 69 | func TestCollection_Flush(t *testing.T) { 70 | err := userCollection.Flush() 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | } 75 | 76 | func BenchmarkCollection_Put(b *testing.B) { 77 | john := user{"John Doe", "john.doe@mail.com", 9898787, time.Now()} 78 | for n := 0; n < b.N; n++ { 79 | userCollection.Put("john", &john) 80 | } 81 | } 82 | 83 | func BenchmarkCollection_Get(b *testing.B) { 84 | john := user{} 85 | for n := 0; n < b.N; n++ { 86 | userCollection.Get("john", &john) 87 | } 88 | } 89 | 90 | func BenchmarkCollection_Has(b *testing.B) { 91 | for n := 0; n < b.N; n++ { 92 | userCollection.Has("john") 93 | } 94 | } 95 | 96 | func BenchmarkCollection_List(b *testing.B) { 97 | for n := 0; n < b.N; n++ { 98 | userCollection.List() 99 | } 100 | } 101 | 102 | func BenchmarkCollection_TotalItem(b *testing.B) { 103 | for n := 0; n < b.N; n++ { 104 | userCollection.TotalItem() 105 | } 106 | } 107 | 108 | func BenchmarkCollection_Remove(b *testing.B) { 109 | for n := 0; n < b.N; n++ { 110 | userCollection.Remove("john") 111 | } 112 | } 113 | 114 | func BenchmarkCollection_Flush(b *testing.B) { 115 | for n := 0; n < b.N; n++ { 116 | userCollection.Flush() 117 | } 118 | } 119 | --------------------------------------------------------------------------------