├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── connect ├── connect.go └── connect_test.go ├── db.go ├── goreleaser.yml ├── logo.png ├── main.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | script: 4 | - go get github.com/schollz/boltdb-server/... 5 | - cd $GOPATH/src/github.com/schollz/boltdb-server && go build 6 | - cd $GOPATH/src/github.com/schollz/boltdb-server && ./boltdb-server & 7 | - cd $GOPATH/src/github.com/schollz/boltdb-server/connect && go test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zack 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | BoltDB Server 5 |
6 | Build Status 7 | Code Coverage 8 | GoDoc 9 |

10 | 11 |

A fancy server for Bolt databases

12 | 13 | *boltdb-server* is a server and package (`connect`) for remotely interfacing 14 | with [boltdb/bolt](https://github.com/boltdb/bolt), a pure-Go embedded key/value database. 15 | 16 | Features 17 | ======== 18 | 19 | - Automatic compression of values 20 | - Simple API for getting, setting, moving, popping and deleting BoltDB data 21 | - Package for adding to your Go programs 22 | 23 | Getting Started 24 | =============== 25 | 26 | ## Installing 27 | 28 | To start using the server, install Go and run `go get`: 29 | 30 | ```sh 31 | $ go get -u github.com/schollz/boltdb-server/... 32 | ``` 33 | 34 | This will retrieve the library and the server. 35 | 36 | ## Run 37 | 38 | Run the server using 39 | 40 | ```sh 41 | $GOPATH/bin/boltdb-server 42 | ``` 43 | 44 | Then you can use the server directly (see API below) or plug in a Go program using the connect package, [see tests for more info](https://github.com/schollz/boltdb-server/blob/master/connect/connect_test.go). 45 | 46 | ## API 47 | 48 | ``` 49 | // Get map of buckets and the number of keys in each 50 | GET /v1/db//stats 51 | 52 | // Get list of all buckets 53 | GET /v1/db//buckets 54 | 55 | // Get all keys and values from a bucket 56 | GET /v1/db//bucket//numkeys 57 | 58 | // Get all keys and values from a bucket 59 | GET /v1/db//bucket//all 60 | 61 | // Get all keys and values specified by ?keys=key1,key2 or by JSON 62 | GET /v1/db//bucket//some 63 | 64 | // Delete and return first n keys 65 | GET /v1/db//bucket//pop?n=X 66 | 67 | // Get all keys in a bucket 68 | GET /v1/db//bucket//keys", handleGetKeys) 69 | 70 | // Return boolean of whether it has key 71 | GET /v1/db//bucket//haskey/ 72 | 73 | // Return boolean of whether any buckets contain any keys specified by JSON 74 | GET /v1/db//haskeys 75 | 76 | // Delete database file 77 | DELETE /v1/db/ 78 | 79 | // Delete bucket 80 | DELETE /v1/db//bucket/ 81 | 82 | // Delete keys, where keys are specified by JSON []string 83 | DELETE /v1/db//bucket//keys 84 | 85 | // Updates a database with keystore specified by JSON 86 | POST /v1/db//bucket//update 87 | 88 | // Move keys, with buckets and keys specified by JSON 89 | POST /v1/db//move 90 | 91 | // Create buckets specified by JSON 92 | POST /v1/db//create 93 | ``` 94 | -------------------------------------------------------------------------------- /connect/connect.go: -------------------------------------------------------------------------------- 1 | // Package connect provides functionality for directly accessing a BoltDB server 2 | // instance with simple functions for posting data, getting data (such as keys and buckets), 3 | // moving data, deleting data, and popping data. 4 | // 5 | // To use, make sure that you have a boltdb-server up and running which you can do simply 6 | // with 7 | // 8 | // go get github.com/schollz/boltdb-server 9 | // $GOPATH/bin/boltdb-server 10 | // 11 | package connect 12 | 13 | import ( 14 | "bytes" 15 | "encoding/json" 16 | "errors" 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | // Connection is the BoltDB server instance 22 | type Connection struct { 23 | DBName string 24 | Address string 25 | } 26 | 27 | // Open will load a connection to BoltDB 28 | func Open(address, dbname string) (*Connection, error) { 29 | c := new(Connection) 30 | c.Address = address 31 | c.DBName = dbname 32 | resp, err := http.Get(c.Address + "/v1/uptime") 33 | if err != nil { 34 | return c, err 35 | } 36 | defer resp.Body.Close() 37 | return c, nil 38 | } 39 | 40 | // DeleteDatabase deletes the database 41 | func (c *Connection) DeleteDatabase() error { 42 | req, err := http.NewRequest("DELETE", c.Address+"/v1/db/"+c.DBName, nil) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | resp, err := http.DefaultClient.Do(req) 48 | if err != nil { 49 | return err 50 | } 51 | defer resp.Body.Close() 52 | return nil 53 | } 54 | 55 | // CreateBuckets inserts some buckets into the DB 56 | func (c *Connection) CreateBuckets(buckets []string) error { 57 | payloadBytes, err := json.Marshal(buckets) 58 | if err != nil { 59 | return err 60 | } 61 | body := bytes.NewReader(payloadBytes) 62 | 63 | req, err := http.NewRequest("POST", c.Address+"/v1/db/"+c.DBName+"/create", body) 64 | if err != nil { 65 | return err 66 | } 67 | req.Header.Set("Content-Type", "application/json") 68 | 69 | resp, err := http.DefaultClient.Do(req) 70 | if err != nil { 71 | return err 72 | } 73 | defer resp.Body.Close() 74 | return nil 75 | } 76 | 77 | // Post keys and values to database 78 | func (c *Connection) Post(bucket string, keystore map[string]string) error { 79 | payloadBytes, err := json.Marshal(keystore) 80 | if err != nil { 81 | return err 82 | } 83 | body := bytes.NewReader(payloadBytes) 84 | 85 | req, err := http.NewRequest("POST", c.Address+"/v1/db/"+c.DBName+"/bucket/"+bucket+"/update", body) 86 | if err != nil { 87 | return err 88 | } 89 | req.Header.Set("Content-Type", "application/json") 90 | 91 | resp, err := http.DefaultClient.Do(req) 92 | if err != nil { 93 | return err 94 | } 95 | defer resp.Body.Close() 96 | return nil 97 | } 98 | 99 | // Get keys and values from database 100 | func (c *Connection) Get(bucket string, keys []string) (map[string]string, error) { 101 | payloadBytes, err := json.Marshal(keys) 102 | if err != nil { 103 | return make(map[string]string), err 104 | } 105 | body := bytes.NewReader(payloadBytes) 106 | 107 | req, err := http.NewRequest("GET", c.Address+"/v1/db/"+c.DBName+"/bucket/"+bucket+"/some", body) 108 | if err != nil { 109 | return make(map[string]string), err 110 | } 111 | req.Header.Set("Content-Type", "application/json") 112 | 113 | resp, err := http.DefaultClient.Do(req) 114 | if err != nil { 115 | return make(map[string]string), err 116 | } 117 | defer resp.Body.Close() 118 | 119 | var target map[string]string 120 | err = json.NewDecoder(resp.Body).Decode(&target) 121 | if err != nil { 122 | return make(map[string]string), err 123 | } 124 | return target, nil 125 | } 126 | 127 | // GetAll keys and values from database 128 | func (c *Connection) GetAll(bucket string) (map[string]string, error) { 129 | resp, err := http.Get(c.Address + "/v1/db/" + c.DBName + "/bucket/" + bucket + "/all") 130 | if err != nil { 131 | return make(map[string]string), err 132 | } 133 | defer resp.Body.Close() 134 | 135 | var target map[string]string 136 | err = json.NewDecoder(resp.Body).Decode(&target) 137 | if err != nil { 138 | return make(map[string]string), err 139 | } 140 | return target, nil 141 | } 142 | 143 | // GetKeys returns all keys from database 144 | func (c *Connection) GetKeys(bucket string) ([]string, error) { 145 | resp, err := http.Get(c.Address + "/v1/db/" + c.DBName + "/bucket/" + bucket + "/keys") 146 | if err != nil { 147 | return []string{}, err 148 | } 149 | defer resp.Body.Close() 150 | 151 | var target []string 152 | err = json.NewDecoder(resp.Body).Decode(&target) 153 | return target, err 154 | } 155 | 156 | // Pop returns and deletes the first n keys from a bucket 157 | func (c *Connection) Pop(bucket string, n int) (keystore map[string]string, err error) { 158 | resp, err := http.Get(fmt.Sprintf("%s/v1/db/%s/bucket/%s/pop?n=%d", c.Address, c.DBName, bucket, n)) 159 | if err != nil { 160 | return keystore, err 161 | } 162 | defer resp.Body.Close() 163 | 164 | err = json.NewDecoder(resp.Body).Decode(&keystore) 165 | return keystore, err 166 | } 167 | 168 | // HasKey checks whether a key exists, or not, in a bucket 169 | func (c *Connection) HasKey(bucket string, key string) (doesHaveKey bool, err error) { 170 | doesHaveKey = false 171 | resp, err := http.Get(fmt.Sprintf("%s/v1/db/%s/bucket/%s/haskey/%s", c.Address, c.DBName, bucket, key)) 172 | if err != nil { 173 | return doesHaveKey, err 174 | } 175 | defer resp.Body.Close() 176 | 177 | err = json.NewDecoder(resp.Body).Decode(&doesHaveKey) 178 | return doesHaveKey, err 179 | } 180 | 181 | // HasKeys checks whether any of the specified keys exist in any buckets. 182 | // Returns a map of the keys and a boolean of whether they are found. 183 | func (c *Connection) HasKeys(buckets []string, keys []string) (doesHaveKeyMap map[string]bool, err error) { 184 | type QueryJSON struct { 185 | Buckets []string `json:"buckets"` 186 | Keys []string `json:"keys"` 187 | } 188 | payloadJSON := new(QueryJSON) 189 | payloadJSON.Buckets = buckets 190 | payloadJSON.Keys = keys 191 | 192 | payloadBytes, err := json.Marshal(payloadJSON) 193 | if err != nil { 194 | return doesHaveKeyMap, err 195 | } 196 | body := bytes.NewReader(payloadBytes) 197 | 198 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/db/%s/haskeys", c.Address, c.DBName), body) 199 | if err != nil { 200 | return doesHaveKeyMap, err 201 | } 202 | req.Header.Set("Content-Type", "application/json") 203 | 204 | resp, err := http.DefaultClient.Do(req) 205 | if err != nil { 206 | return doesHaveKeyMap, err 207 | } 208 | defer resp.Body.Close() 209 | 210 | err = json.NewDecoder(resp.Body).Decode(&doesHaveKeyMap) 211 | return doesHaveKeyMap, err 212 | } 213 | 214 | // Move moves a list of keys from one bucket to another. This function will 215 | // create the second bucket if it does not exist. 216 | func (c *Connection) Move(bucket string, bucket2 string, keys []string) (err error) { 217 | type QueryJSON struct { 218 | FromBucket string `json:"from_bucket"` 219 | ToBucket string `json:"to_bucket"` 220 | Keys []string `json:"keys"` 221 | } 222 | moveJSON := new(QueryJSON) 223 | moveJSON.FromBucket = bucket 224 | moveJSON.ToBucket = bucket2 225 | moveJSON.Keys = keys 226 | 227 | payloadBytes, err := json.Marshal(moveJSON) 228 | if err != nil { 229 | return err 230 | } 231 | body := bytes.NewReader(payloadBytes) 232 | 233 | req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/db/%s/move", c.Address, c.DBName), body) 234 | if err != nil { 235 | return err 236 | } 237 | req.Header.Set("Content-Type", "application/json") 238 | 239 | resp, err := http.DefaultClient.Do(req) 240 | if err != nil { 241 | return err 242 | } 243 | defer resp.Body.Close() 244 | return nil 245 | } 246 | 247 | // Stats returns a list of buckets and number of keys in each 248 | func (c *Connection) Stats() (stats map[string]int, err error) { 249 | resp, err := http.Get(fmt.Sprintf("%s/v1/db/%s/stats", c.Address, c.DBName)) 250 | if err != nil { 251 | return stats, err 252 | } 253 | defer resp.Body.Close() 254 | if resp.StatusCode == 200 { 255 | err = json.NewDecoder(resp.Body).Decode(&stats) 256 | } else { 257 | err = errors.New("Problem getting stats") 258 | } 259 | return stats, err 260 | } 261 | -------------------------------------------------------------------------------- /connect/connect_test.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | // Start server with 12 | // go build; .\boltdb-server.exe 13 | var testingServer = "http://localhost:8050" 14 | 15 | func BenchmarkPost1(b *testing.B) { 16 | os.Remove(path.Join("..", "dbs", "testbench.db")) 17 | conn, _ := Open(testingServer, "testbench") 18 | b.ResetTimer() 19 | for i := 0; i < b.N; i++ { 20 | m := map[string]string{"key" + strconv.Itoa(i): "value" + strconv.Itoa(i)} 21 | conn.Post("benchkeys", m) 22 | } 23 | } 24 | 25 | func BenchmarkGetAll1(b *testing.B) { 26 | conn, _ := Open(testingServer, "testbench") 27 | b.ResetTimer() 28 | for i := 0; i < b.N; i++ { 29 | conn.GetAll("benchkeys") 30 | } 31 | } 32 | 33 | func BenchmarkPost1000(b *testing.B) { 34 | conn, _ := Open(testingServer, "testbench") 35 | m := make(map[string]string) 36 | for i := 0; i < 1000; i++ { 37 | m["key"+strconv.Itoa(i)] = "value" + strconv.Itoa(i) 38 | } 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | conn.Post("benchkeys", m) 42 | } 43 | } 44 | 45 | func BenchmarkGetAll1000(b *testing.B) { 46 | conn, _ := Open(testingServer, "testbench") 47 | b.ResetTimer() 48 | for i := 0; i < b.N; i++ { 49 | conn.GetAll("benchkeys") 50 | } 51 | } 52 | 53 | func BenchmarkPost100000(b *testing.B) { 54 | conn, _ := Open(testingServer, "testbench") 55 | m := make(map[string]string) 56 | for i := 0; i < 100000; i++ { 57 | m["key"+strconv.Itoa(i)] = "value" + strconv.Itoa(i) 58 | } 59 | b.ResetTimer() 60 | for i := 0; i < b.N; i++ { 61 | conn.Post("benchkeys", m) 62 | } 63 | } 64 | 65 | func BenchmarkGetAll100000(b *testing.B) { 66 | conn, _ := Open(testingServer, "testbench") 67 | b.ResetTimer() 68 | for i := 0; i < b.N; i++ { 69 | conn.GetAll("benchkeys") 70 | } 71 | } 72 | 73 | func BenchmarkGetStats100000(b *testing.B) { 74 | conn, _ := Open(testingServer, "testbench") 75 | b.ResetTimer() 76 | for i := 0; i < b.N; i++ { 77 | conn.Stats() 78 | } 79 | } 80 | 81 | func BenchmarkGetKeys(b *testing.B) { 82 | conn, _ := Open(testingServer, "testbench") 83 | b.ResetTimer() 84 | for i := 0; i < b.N; i++ { 85 | conn.GetKeys("benchkeys") 86 | } 87 | } 88 | 89 | func BenchmarkGetTwo(b *testing.B) { 90 | conn, _ := Open(testingServer, "testbench") 91 | b.ResetTimer() 92 | for i := 0; i < b.N; i++ { 93 | conn.Get("benchkeys", []string{"key1", "key20"}) 94 | } 95 | } 96 | 97 | func BenchmarkHasKey(b *testing.B) { 98 | conn, _ := Open(testingServer, "testbench") 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | conn.HasKey("benchkeys", "key1") 102 | } 103 | } 104 | 105 | func TestGeneral(t *testing.T) { 106 | // Test opening DB that doesnt exist 107 | conn, err := Open("http://asdkfjalsjdflkasjdf", "testdb") 108 | if err == nil { 109 | t.Errorf("Should not be able to connect") 110 | } 111 | 112 | // Test opening DB 113 | conn, err = Open(testingServer, "testdb") 114 | if err != nil { 115 | t.Errorf(err.Error()) 116 | } 117 | 118 | err = conn.CreateBuckets([]string{"people_locations"}) 119 | if err != nil { 120 | t.Errorf(err.Error()) 121 | } 122 | 123 | data := make(map[string]string) 124 | data["zack"] = "canada" 125 | data["jessie"] = "usa" 126 | err = conn.Post("people_locations", data) 127 | if err != nil { 128 | t.Errorf(err.Error()) 129 | } 130 | if _, err := os.Stat(path.Join("..", "dbs", "testdb.db")); os.IsNotExist(err) { 131 | t.Errorf("Problem creating directory") 132 | } 133 | 134 | // Test HasKeys 135 | data["bob"] = "brazil" 136 | data["jill"] = "antarctica" 137 | err = conn.Post("people_locations3", data) 138 | hasKeysMap, err := conn.HasKeys([]string{"people_locations", "people_locations3"}, []string{"zack", "jessie", "bob", "jim"}) 139 | if err != nil { 140 | t.Errorf(err.Error()) 141 | } 142 | if hasKeysMap["jim"] != false || hasKeysMap["bob"] != true || len(hasKeysMap) != 4 { 143 | t.Errorf("Problem checking whether buckets have keys") 144 | } 145 | 146 | // Test HasKey 147 | hasKey, err := conn.HasKey("people_locations", "zack") 148 | if err != nil { 149 | t.Errorf(err.Error()) 150 | } 151 | if hasKey == false { 152 | t.Errorf("Incorrectly checking whether key exists") 153 | } 154 | hasKey, err = conn.HasKey("people_locations", "askjdflasjdlkfj") 155 | if err != nil { 156 | t.Errorf(err.Error()) 157 | } 158 | if hasKey != false { 159 | t.Errorf("Incorrectly checking whether key exists") 160 | } 161 | 162 | data2, err := conn.GetAll("people_locations") 163 | if err != nil { 164 | t.Errorf(err.Error()) 165 | } 166 | if val, ok := data2["zack"]; ok { 167 | if val != "canada" { 168 | t.Errorf("Could not get zack back: %v", val) 169 | } 170 | } else { 171 | t.Errorf("Problem with GetAll: %v", data2) 172 | } 173 | 174 | keys, err := conn.GetKeys("people_locations") 175 | if err != nil { 176 | t.Errorf(err.Error()) 177 | } 178 | if len(keys) != 2 { 179 | fmt.Println(keys) 180 | t.Errorf("Problem getting the two keys back") 181 | } 182 | 183 | // Test Pop 184 | keystore, err := conn.Pop("people_locations", 1) 185 | if err != nil { 186 | t.Errorf(err.Error()) 187 | } 188 | if val, ok := keystore["jessie"]; ok { 189 | if val != "usa" { 190 | t.Errorf("Could not get jessie back") 191 | } 192 | } else { 193 | t.Errorf("Problem with Pop: %v", keystore) 194 | } 195 | keys, err = conn.GetKeys("people_locations") 196 | if err != nil { 197 | t.Errorf(err.Error()) 198 | } 199 | if len(keys) != 1 { 200 | fmt.Println(keys) 201 | t.Errorf("Problem getting the one keys back") 202 | } 203 | 204 | // Test Move 205 | keys, _ = conn.GetKeys("people_locations") 206 | err = conn.Move("people_locations", "people_locations2", keys) 207 | if err != nil { 208 | t.Errorf(err.Error()) 209 | } 210 | keys, err = conn.GetKeys("people_locations2") 211 | if err != nil { 212 | t.Errorf(err.Error()) 213 | } 214 | if len(keys) != 1 { 215 | t.Errorf("Problem getting the one keys back") 216 | } 217 | keys, err = conn.GetKeys("people_locations") 218 | if err != nil { 219 | t.Errorf(err.Error()) 220 | } 221 | if len(keys) != 0 { 222 | t.Errorf("Problem getting the one keys back") 223 | } 224 | 225 | // Test getting bucket that doesn't exist 226 | _, err = conn.GetKeys("asldkfjaslkdjf") 227 | if err == nil { 228 | t.Errorf("Should throw error, bucket does not exist") 229 | } 230 | 231 | // Test getting stats 232 | stats, err := conn.Stats() 233 | if err != nil { 234 | t.Error(err) 235 | } 236 | if stats["people_locations3"] != 4 { 237 | t.Errorf("Problem with getting stats: %v", stats) 238 | } 239 | 240 | // Test deleting database 241 | err = conn.DeleteDatabase() 242 | if err != nil { 243 | t.Error(err) 244 | } 245 | if _, err := os.Stat(path.Join("..", "dbs", "testdb.db")); os.IsExist(err) { 246 | t.Errorf("Problem deleting db") 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "sync" 9 | "time" 10 | 11 | "github.com/boltdb/bolt" 12 | ) 13 | 14 | var dbs = struct { 15 | sync.RWMutex 16 | data map[string]*DBData 17 | }{data: make(map[string]*DBData)} 18 | 19 | type DBData struct { 20 | lastEdited time.Time 21 | db *bolt.DB 22 | } 23 | 24 | func init() { 25 | go closeDBs() 26 | } 27 | 28 | func getDB(dbname string) (*bolt.DB, error) { 29 | dbs.Lock() 30 | defer dbs.Unlock() 31 | var err error 32 | if _, ok := dbs.data[dbname]; !ok { 33 | log.Debug("Opening %s", dbname) 34 | tempDB, err2 := bolt.Open(path.Join(dbpath, dbname+".db"), 0755, nil) 35 | dbs.data[dbname] = new(DBData) 36 | dbs.data[dbname].db = tempDB 37 | err = err2 38 | } 39 | dbs.data[dbname].lastEdited = time.Now() 40 | db := dbs.data[dbname].db 41 | return db, err 42 | } 43 | 44 | func closeDBs() { 45 | for { 46 | time.Sleep(10 * time.Second) 47 | dbs.Lock() 48 | toDelete := []string{} 49 | for dbname := range dbs.data { 50 | if time.Since(dbs.data[dbname].lastEdited).Seconds() > 10 { 51 | toDelete = append(toDelete, dbname) 52 | } 53 | } 54 | dbs.Unlock() 55 | 56 | for _, dbname := range toDelete { 57 | log.Debug("Closing %s", dbname) 58 | deleteDB(dbname) 59 | } 60 | } 61 | } 62 | 63 | func deleteDB(dbname string) { 64 | dbs.Lock() 65 | if _, ok := dbs.data[dbname]; ok { 66 | dbs.data[dbname].db.Close() 67 | delete(dbs.data, dbname) 68 | } 69 | dbs.Unlock() 70 | } 71 | 72 | func getNumberKeysInBucket(dbname string, bucket string) (n int, err error) { 73 | n = 0 74 | db, err := getDB(dbname) 75 | if err != nil { 76 | return n, err 77 | } 78 | 79 | err = db.View(func(tx *bolt.Tx) error { 80 | b := tx.Bucket([]byte(bucket)) 81 | if b == nil { 82 | return errors.New("Bucket does not exist") 83 | } 84 | c := b.Cursor() 85 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 86 | n++ 87 | } 88 | return nil 89 | }) 90 | log.Trace("Found %d keys in bucket '%s' in db '%s'", n, bucket, dbname) 91 | return n, err 92 | } 93 | 94 | func getBucketNames(dbname string) (bucketNames []string, err error) { 95 | db, err := getDB(dbname) 96 | if err != nil { 97 | return bucketNames, err 98 | } 99 | 100 | err = db.View(func(tx *bolt.Tx) error { 101 | return tx.ForEach(func(name []byte, _ *bolt.Bucket) error { 102 | bucketNames = append(bucketNames, string(name)) 103 | return nil 104 | }) 105 | }) 106 | return bucketNames, err 107 | } 108 | 109 | func createDatabase(dbname string, buckets []string) error { 110 | db, err := getDB(dbname) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return db.Update(func(tx *bolt.Tx) error { 116 | for _, bucket := range buckets { 117 | _, err2 := tx.CreateBucketIfNotExists([]byte(bucket)) 118 | if err2 != nil { 119 | return err2 120 | } 121 | } 122 | return nil 123 | }) 124 | } 125 | 126 | // updateDatabase 127 | func updateDatabase(dbname string, bucket string, keystore map[string]string) error { 128 | db, err := getDB(dbname) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | return db.Update(func(tx *bolt.Tx) error { 134 | b, err2 := tx.CreateBucketIfNotExists([]byte(bucket)) 135 | if err2 != nil { 136 | return err2 137 | } 138 | for key, value := range keystore { 139 | err2 := b.Put([]byte(key), compressStringToByte(value)) 140 | if err2 != nil { 141 | return err 142 | } 143 | } 144 | return err 145 | }) 146 | } 147 | 148 | func getKeysFromDatabase(dbname string, bucket string) (keys []string, err error) { 149 | db, err := getDB(dbname) 150 | if err != nil { 151 | return []string{}, err 152 | } 153 | 154 | numKeys := 0 155 | err = db.View(func(tx *bolt.Tx) error { 156 | // Assume bucket exists and has keys 157 | b := tx.Bucket([]byte(bucket)) 158 | if b == nil { 159 | return errors.New("Bucket does not exist") 160 | } 161 | c := b.Cursor() 162 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 163 | numKeys++ 164 | } 165 | 166 | keys = make([]string, numKeys) 167 | numKeys = 0 168 | c = b.Cursor() 169 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 170 | keys[numKeys] = string(k) 171 | numKeys++ 172 | } 173 | return nil 174 | }) 175 | return 176 | } 177 | 178 | func getFromDatabase(dbname string, bucket string, keys []string) (map[string]string, error) { 179 | keystore := make(map[string]string) 180 | 181 | db, err := getDB(dbname) 182 | if err != nil { 183 | return keystore, err 184 | } 185 | 186 | if len(keys) == 0 { 187 | // Get all keys 188 | err = db.View(func(tx *bolt.Tx) error { 189 | // Assume bucket exists and has keys 190 | b := tx.Bucket([]byte(bucket)) 191 | if b == nil { 192 | return errors.New("Bucket does not exist") 193 | } 194 | c := b.Cursor() 195 | for k, v := c.First(); k != nil; k, v = c.Next() { 196 | if v != nil { 197 | keystore[string(k)] = decompressByteToString(v) 198 | } 199 | } 200 | return nil 201 | }) 202 | } else { 203 | // Get specified keys 204 | err = db.View(func(tx *bolt.Tx) error { 205 | b := tx.Bucket([]byte(bucket)) 206 | if b == nil { 207 | return errors.New("Bucket does not exist") 208 | } 209 | for _, key := range keys { 210 | v := b.Get([]byte(key)) 211 | if v != nil { 212 | keystore[key] = decompressByteToString(v) 213 | } 214 | } 215 | return nil 216 | }) 217 | } 218 | return keystore, err 219 | } 220 | 221 | func deleteDatabase(dbname string) error { 222 | // Check if there is a specific match in the dbpath, otherwise it may 223 | // be an attack 224 | files, _ := ioutil.ReadDir(path.Join(dbpath)) 225 | foundDB := false 226 | for _, f := range files { 227 | if f.Name() == dbname+".db" { 228 | foundDB = true 229 | } 230 | } 231 | if !foundDB { 232 | return errors.New("Could not find '" + dbname + "'") 233 | } 234 | 235 | _, err := getDB(dbname) 236 | if err != nil { 237 | return err 238 | } 239 | deleteDB(dbname) 240 | 241 | if _, err := os.Stat(path.Join(dbpath, dbname+".db")); os.IsNotExist(err) { 242 | return err 243 | } 244 | return os.Remove(path.Join(dbpath, dbname+".db")) 245 | } 246 | 247 | func deleteKeys(dbname string, bucket string, keys []string) error { 248 | db, err := getDB(dbname) 249 | if err != nil { 250 | return err 251 | } 252 | 253 | return db.Update(func(tx *bolt.Tx) error { 254 | b := tx.Bucket([]byte(bucket)) 255 | if b == nil { 256 | return errors.New("Bucket does not exist") 257 | } 258 | for _, key := range keys { 259 | b.Delete([]byte(key)) 260 | } 261 | return err 262 | }) 263 | } 264 | 265 | func deleteBucket(dbname string, bucket string) error { 266 | db, err := getDB(dbname) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | return db.Update(func(tx *bolt.Tx) error { 272 | return tx.DeleteBucket([]byte(bucket)) 273 | }) 274 | } 275 | 276 | func pop(dbname string, bucket string, n int) (map[string]string, error) { 277 | keystore := make(map[string]string) 278 | 279 | db, err := getDB(dbname) 280 | if err != nil { 281 | return keystore, err 282 | } 283 | 284 | err = db.Update(func(tx *bolt.Tx) error { 285 | b := tx.Bucket([]byte(bucket)) 286 | if b == nil { 287 | return errors.New("Bucket does not exist") 288 | } 289 | 290 | c := b.Cursor() 291 | for k, v := c.First(); k != nil; k, v = c.Next() { 292 | b.Delete(k) 293 | keystore[string(k)] = decompressByteToString(v) 294 | if len(keystore) == n { 295 | break 296 | } 297 | } 298 | return nil 299 | }) 300 | return keystore, err 301 | } 302 | 303 | func moveBuckets(dbname string, bucket1 string, bucket2 string, keys []string) error { 304 | db, err := getDB(dbname) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | return db.Update(func(tx *bolt.Tx) error { 310 | b := tx.Bucket([]byte(bucket1)) 311 | if b == nil { 312 | return errors.New("Bucket does not exist") 313 | } 314 | b2, _ := tx.CreateBucketIfNotExists([]byte(bucket2)) 315 | for _, key := range keys { 316 | val := b.Get([]byte(key)) 317 | if val != nil { 318 | b.Delete([]byte(key)) 319 | b2.Put([]byte(key), val) 320 | } else { 321 | return errors.New("Could not find key: " + key) 322 | } 323 | } 324 | return nil 325 | }) 326 | } 327 | 328 | func hasKeys(dbname string, buckets []string, keys []string) (doesHaveKeyMap map[string]bool, err error) { 329 | doesHaveKeyMap = make(map[string]bool) 330 | 331 | db, err := getDB(dbname) 332 | if err != nil { 333 | return doesHaveKeyMap, err 334 | } 335 | 336 | for _, key := range keys { 337 | doesHaveKeyMap[key] = false 338 | } 339 | 340 | err = db.View(func(tx *bolt.Tx) error { 341 | for _, bucket := range buckets { 342 | b := tx.Bucket([]byte(bucket)) 343 | if b == nil { 344 | continue 345 | } 346 | for _, key := range keys { 347 | v := b.Get([]byte(key)) 348 | if v != nil { 349 | doesHaveKeyMap[string(key)] = true 350 | } 351 | } 352 | } 353 | return nil 354 | }) 355 | return doesHaveKeyMap, err 356 | } 357 | 358 | func hasKey(dbname string, bucket string, key string) (doesHaveKey bool, err error) { 359 | doesHaveKey = false 360 | 361 | db, err := getDB(dbname) 362 | if err != nil { 363 | return doesHaveKey, err 364 | } 365 | 366 | err = db.View(func(tx *bolt.Tx) error { 367 | b := tx.Bucket([]byte(bucket)) 368 | if b == nil { 369 | return errors.New("Bucket does not exist") 370 | } 371 | v := b.Get([]byte(key)) 372 | if v != nil { 373 | doesHaveKey = true 374 | } 375 | return nil 376 | }) 377 | return doesHaveKey, err 378 | } 379 | -------------------------------------------------------------------------------- /goreleaser.yml: -------------------------------------------------------------------------------- 1 | # goreleaser.yml 2 | # Build customization 3 | build: 4 | binary_name: boltdb-server 5 | goos: 6 | - windows 7 | - darwin 8 | - linux 9 | goarch: 10 | - amd64 11 | # Archive customization 12 | archive: 13 | format: zip 14 | replacements: 15 | amd64: 64-bit 16 | darwin: macOS 17 | linux: linux 18 | windows: windows 19 | files: 20 | - README.md 21 | - LICENSE 22 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schollz/boltdb-server/023403d380dcfd94517a2e14c8e4006e293a5025/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/jcelliott/lumber" 14 | "gopkg.in/urfave/cli.v1" 15 | ) 16 | 17 | // Testing 18 | // curl -H "Content-Type: application/json" -X POST -d '{"bucket":"food","keystore":{"username":"xyz","password":"xyz"}}' 'http://zack:123@localhost:8080/' 19 | 20 | var version string 21 | var port, dbpath string 22 | var compressOn, verbose bool 23 | var log *lumber.ConsoleLogger 24 | 25 | func main() { 26 | // flag.BoolVar(&verbose, "verbose", false, "verbosity") 27 | // flag.BoolVar(&compressOn, "compress", false, "use compression") 28 | // flag.StringVar(&dbpath, "db", path.Join(".", "dbs"), "path to the database") 29 | // flag.StringVar(&port, "port", "8080", "port to listen on") 30 | // flag.Parse() 31 | app := cli.NewApp() 32 | app.Name = "boltdb-server" 33 | app.Usage = "fancy server for connecting to a BoltDB keystore" 34 | app.Version = version 35 | app.Compiled = time.Now() 36 | app.Action = func(c *cli.Context) error { 37 | dbpath = c.GlobalString("db") 38 | compressOn = c.GlobalBool("compress") 39 | verbose = c.GlobalBool("debug") 40 | port = c.GlobalString("port") 41 | os.MkdirAll(dbpath, 0755) 42 | 43 | if verbose { 44 | log = lumber.NewConsoleLogger(lumber.TRACE) 45 | } else { 46 | log = lumber.NewConsoleLogger(lumber.WARN) 47 | } 48 | 49 | startTime := time.Now() 50 | 51 | gin.SetMode(gin.ReleaseMode) 52 | r := gin.Default() 53 | r.GET("/v1/api", func(c *gin.Context) { 54 | c.String(200, ` 55 | 56 | // Get map of buckets and the number of keys in each 57 | GET /v1/db//stats 58 | 59 | // Get list of all buckets 60 | GET /v1/db//buckets 61 | 62 | // Get all keys and values from a bucket 63 | GET /v1/db//bucket//numkeys 64 | 65 | // Get all keys and values from a bucket 66 | GET /v1/db//bucket//all 67 | 68 | // Get all keys and values specified by ?keys=key1,key2 or by JSON 69 | GET /v1/db//bucket//some 70 | 71 | // Delete and return first n keys 72 | GET /v1/db//bucket//pop?n=X 73 | 74 | // Get all keys in a bucket 75 | GET /v1/db//bucket//keys", handleGetKeys) 76 | 77 | // Return boolean of whether it has key 78 | GET /v1/db//bucket//haskey/ 79 | 80 | // Return boolean of whether any buckets contain any keys specified by JSON 81 | GET /v1/db//haskeys 82 | 83 | // Delete database file 84 | DELETE /v1/db/ 85 | 86 | // Delete bucket 87 | DELETE /v1/db//bucket/ 88 | 89 | // Delete keys, where keys are specified by JSON []string 90 | DELETE /v1/db//bucket//keys 91 | 92 | // Updates a database with keystore specified by JSON 93 | POST /v1/db//bucket//update 94 | 95 | // Move keys, with buckets and keys specified by JSON 96 | POST /v1/db//move 97 | 98 | // Create buckets specified by JSON 99 | POST /v1/db//create 100 | 101 | `) 102 | }) 103 | r.GET("/v1/uptime", func(c *gin.Context) { 104 | c.JSON(200, gin.H{ 105 | "uptime": time.Since(startTime).String(), 106 | }) 107 | }) 108 | r.GET("/v1/db/:dbname/stats", handleGetDBStats) // Get map of buckets and the number of keys in each 109 | r.GET("/v1/db/:dbname/buckets", handleGetBuckets) // Get list of all buckets 110 | r.GET("/v1/db/:dbname/bucket/:bucket/numkeys", handleGetNumKeys) // Get all keys and values from a bucket (no parameters) 111 | r.GET("/v1/db/:dbname/bucket/:bucket/all", handleGet) // Get all keys and values from a bucket (no parameters) 112 | r.GET("/v1/db/:dbname/bucket/:bucket/some", handleGet) // Get all keys and values specified by ?keys=key1,key2 or by JSON 113 | r.GET("/v1/db/:dbname/bucket/:bucket/pop", handlePop) // Delete and return first n keys + values, where n specified by ?n=100 114 | r.GET("/v1/db/:dbname/bucket/:bucket/keys", handleGetKeys) // Get all keys in a bucket (no parameters) 115 | r.GET("/v1/db/:dbname/bucket/:bucket/haskey/:key", handleHasKey) // Return boolean of whether it has key 116 | r.GET("/v1/db/:dbname/haskeys", handleHasKeys) // Return boolean of whether any of the buckets contain the keys 117 | // TODO: r.GET("/v1/db/:dbname/bucket/:bucket/data", getDataArchive) // Creates archive with keys as filenames and values as contents, returns archive 118 | 119 | r.DELETE("/v1/db/:dbname", handleDeleteDatabase) // Delete database file (no parameters) 120 | r.DELETE("/v1/db/:dbname/bucket/:bucket", handleDeleteBucket) // Delete bucket (no parameters) 121 | r.DELETE("/v1/db/:dbname/bucket/:bucket/keys", handleDeleteKeys) // Delete keys, where keys are specified by JSON []string 122 | // 123 | r.POST("/v1/db/:dbname/bucket/:bucket/update", handleUpdate) // Updates a database with keystore specified by JSON 124 | r.POST("/v1/db/:dbname/move", handleMove) // Move keys, with buckets and keys specified by JSON 125 | r.POST("/v1/db/:dbname/create", handleCreateDB) // Move keys, with buckets and keys specified by JSON 126 | 127 | fmt.Printf("boltdb-server (v.%s) running on http://%s:%s\n", version, GetLocalIP(), port) 128 | r.Run(":" + port) // listen and serve on 0.0.0.0:8080 129 | return nil 130 | } 131 | app.Flags = []cli.Flag{ 132 | cli.StringFlag{ 133 | Name: "port, p", 134 | Value: "8050", 135 | Usage: "port to use to listen", 136 | }, 137 | cli.StringFlag{ 138 | Name: "db", 139 | Value: path.Join(".", "dbs"), 140 | Usage: "path to use for storing databases", 141 | }, 142 | cli.BoolFlag{ 143 | Name: "compress, c", 144 | Usage: "turn on compression", 145 | }, 146 | cli.BoolFlag{ 147 | Name: "debug", 148 | Usage: "turn on debug mode", 149 | }, 150 | } 151 | app.Run(os.Args) 152 | 153 | } 154 | 155 | func handleHasKeys(c *gin.Context) { 156 | dbname := c.Param("dbname") 157 | 158 | type QueryJSON struct { 159 | Keys []string `json:"keys"` 160 | Buckets []string `json:"buckets"` 161 | } 162 | var json QueryJSON 163 | if c.BindJSON(&json) != nil { 164 | c.String(http.StatusBadRequest, "Problem binding keys") 165 | return 166 | } 167 | 168 | doesHaveKeyMap, err := hasKeys(dbname, json.Buckets, json.Keys) 169 | if err != nil { 170 | c.String(http.StatusInternalServerError, err.Error()) 171 | return 172 | } 173 | c.JSON(http.StatusOK, doesHaveKeyMap) 174 | } 175 | 176 | func handleCreateDB(c *gin.Context) { 177 | dbname := c.Param("dbname") 178 | 179 | var json []string 180 | if c.BindJSON(&json) != nil { 181 | c.String(http.StatusBadRequest, "Problem binding keys") 182 | return 183 | } 184 | 185 | err := createDatabase(dbname, json) 186 | if err != nil { 187 | c.String(http.StatusInternalServerError, err.Error()) 188 | return 189 | } 190 | c.JSON(http.StatusOK, "Created db") 191 | } 192 | 193 | func handleHasKey(c *gin.Context) { 194 | dbname := c.Param("dbname") 195 | bucket := c.Param("bucket") 196 | key := c.Param("key") 197 | doesHaveKey, err := hasKey(dbname, bucket, key) 198 | if err != nil { 199 | c.String(http.StatusInternalServerError, err.Error()) 200 | return 201 | } 202 | c.JSON(http.StatusOK, doesHaveKey) 203 | } 204 | 205 | func handleGetDBStats(c *gin.Context) { 206 | dbname := c.Param("dbname") 207 | bucketNames, err := getBucketNames(dbname) 208 | if err != nil { 209 | c.String(http.StatusInternalServerError, err.Error()) 210 | return 211 | } 212 | stats := make(map[string]int) 213 | for _, bucket := range bucketNames { 214 | stats[bucket], err = getNumberKeysInBucket(dbname, bucket) 215 | if err != nil { 216 | c.String(http.StatusInternalServerError, err.Error()) 217 | return 218 | } 219 | } 220 | c.JSON(http.StatusOK, stats) 221 | } 222 | 223 | func handleGetNumKeys(c *gin.Context) { 224 | dbname := c.Param("dbname") 225 | bucket := c.Param("bucket") 226 | n, err := getNumberKeysInBucket(dbname, bucket) 227 | if err != nil { 228 | c.String(http.StatusInternalServerError, err.Error()) 229 | return 230 | } 231 | c.JSON(http.StatusOK, n) 232 | } 233 | 234 | func handleGetBuckets(c *gin.Context) { 235 | dbname := c.Param("dbname") 236 | bucketNames, err := getBucketNames(dbname) 237 | if err != nil { 238 | c.String(http.StatusInternalServerError, err.Error()) 239 | return 240 | } 241 | c.JSON(http.StatusOK, bucketNames) 242 | } 243 | 244 | func handleDeleteDatabase(c *gin.Context) { 245 | dbname := c.Param("dbname") 246 | err := deleteDatabase(dbname) 247 | if err != nil { 248 | c.String(http.StatusInternalServerError, err.Error()) 249 | return 250 | } 251 | c.String(http.StatusOK, "Deleted database") 252 | } 253 | 254 | func handleDeleteBucket(c *gin.Context) { 255 | dbname := c.Param("dbname") 256 | bucket := c.Param("bucket") 257 | err := deleteBucket(dbname, bucket) 258 | if err != nil { 259 | c.String(http.StatusInternalServerError, err.Error()) 260 | return 261 | } 262 | c.String(http.StatusOK, "Deleted bucket") 263 | } 264 | 265 | func handleDeleteKeys(c *gin.Context) { 266 | dbname := c.Param("dbname") 267 | bucket := c.Param("bucket") 268 | var keys []string 269 | if c.BindJSON(&keys) != nil { 270 | c.String(http.StatusBadRequest, "Problem binding keys") 271 | return 272 | } 273 | err := deleteKeys(dbname, bucket, keys) 274 | if err != nil { 275 | c.String(http.StatusInternalServerError, err.Error()) 276 | return 277 | } 278 | c.String(http.StatusOK, "Deleted keys") 279 | } 280 | 281 | func handleUpdate(c *gin.Context) { 282 | dbname := c.Param("dbname") 283 | bucket := c.Param("bucket") 284 | var json map[string]string 285 | if c.BindJSON(&json) != nil { 286 | c.String(http.StatusBadRequest, "Problem binding keystore") 287 | return 288 | } 289 | err := updateDatabase(dbname, bucket, json) 290 | if err != nil { 291 | c.String(http.StatusInternalServerError, err.Error()) 292 | return 293 | } 294 | c.String(http.StatusOK, fmt.Sprintf("Inserted %d things into %s", len(json), bucket)) 295 | } 296 | 297 | func handleGetKeys(c *gin.Context) { 298 | dbname := c.Param("dbname") 299 | bucket := c.Param("bucket") 300 | keystore, err := getKeysFromDatabase(dbname, bucket) 301 | if err != nil { 302 | c.String(http.StatusInternalServerError, err.Error()) 303 | return 304 | } 305 | c.JSON(http.StatusOK, keystore) 306 | } 307 | 308 | func handlePop(c *gin.Context) { 309 | dbname := c.Param("dbname") 310 | bucket := c.Param("bucket") 311 | nQuery := c.DefaultQuery("n", "0") 312 | num, err := strconv.Atoi(nQuery) 313 | if err != nil { 314 | c.String(http.StatusInternalServerError, err.Error()) 315 | return 316 | } 317 | if num <= 0 { 318 | c.String(http.StatusBadRequest, "Must specify n > 0") 319 | return 320 | } 321 | keystore, err := pop(dbname, bucket, num) 322 | if err != nil { 323 | c.String(http.StatusInternalServerError, err.Error()) 324 | return 325 | } 326 | c.JSON(http.StatusOK, keystore) 327 | } 328 | 329 | func handleGet(c *gin.Context) { 330 | dbname := c.Param("dbname") 331 | bucket := c.Param("bucket") 332 | keysQuery := c.DefaultQuery("keys", "") 333 | json := []string{} 334 | if c.BindJSON(&json) != nil && keysQuery != "" { 335 | json = strings.Split(keysQuery, ",") 336 | } 337 | // If requested some without providing keys, throw error 338 | if len(json) == 0 && strings.Contains(c.Request.RequestURI, "some") { 339 | c.String(http.StatusBadRequest, "Must provide keys") 340 | return 341 | } 342 | // Get keys and values 343 | keystore, err := getFromDatabase(dbname, bucket, json) 344 | if err != nil { 345 | c.String(http.StatusInternalServerError, err.Error()) 346 | return 347 | } 348 | c.JSON(http.StatusOK, keystore) 349 | return 350 | } 351 | 352 | func handleMove(c *gin.Context) { 353 | dbname := c.Param("dbname") 354 | type QueryJSON struct { 355 | FromBucket string `json:"from_bucket"` 356 | ToBucket string `json:"to_bucket"` 357 | Keys []string `json:"keys"` 358 | } 359 | var json QueryJSON 360 | if c.BindJSON(&json) != nil { 361 | c.String(http.StatusBadRequest, "Must provide keys, from_bucket and to_bucket") 362 | return 363 | } 364 | // Get keys and values 365 | err := moveBuckets(dbname, json.FromBucket, json.ToBucket, json.Keys) 366 | if err != nil { 367 | log.Error("Could not move %v from %s to %s", json.Keys, json.FromBucket, json.ToBucket) 368 | c.String(http.StatusInternalServerError, err.Error()) 369 | return 370 | } 371 | log.Trace("Moved %v from %s to %s", json.Keys, json.FromBucket, json.ToBucket) 372 | c.JSON(http.StatusOK, fmt.Sprintf("Moved keys")) 373 | } 374 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "io" 7 | "math/rand" 8 | "time" 9 | "net" 10 | "strings" 11 | ) 12 | 13 | // GetLocalIP returns the local ip address 14 | func GetLocalIP() string { 15 | addrs, err := net.InterfaceAddrs() 16 | if err != nil { 17 | return "localhost" 18 | } 19 | bestIP := "localhost" 20 | for _, address := range addrs { 21 | // check the address type and if it is not a loopback the display it 22 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 23 | if ipnet.IP.To4() != nil && (strings.Contains(ipnet.IP.String(), "192.168.1") || strings.Contains(ipnet.IP.String(), "192.168")) { 24 | return ipnet.IP.String() 25 | } 26 | } 27 | } 28 | return bestIP 29 | } 30 | 31 | func compressStringToByte(s string) []byte { 32 | if compressOn { 33 | return compressByte([]byte(s)) 34 | } 35 | return []byte(s) 36 | } 37 | 38 | func decompressByteToString(b []byte) string { 39 | if compressOn { 40 | return string(decompressByte(b)) 41 | } 42 | return string(b) 43 | } 44 | 45 | // compressByte returns a compressed byte slice. 46 | func compressByte(src []byte) []byte { 47 | compressedData := new(bytes.Buffer) 48 | compress(src, compressedData, 9) 49 | return compressedData.Bytes() 50 | } 51 | 52 | // decompressByte returns a decompressed byte slice. 53 | func decompressByte(src []byte) []byte { 54 | compressedData := bytes.NewBuffer(src) 55 | deCompressedData := new(bytes.Buffer) 56 | decompress(compressedData, deCompressedData) 57 | return deCompressedData.Bytes() 58 | } 59 | 60 | // compress uses flate to compress a byte slice to a corresponding level 61 | func compress(src []byte, dest io.Writer, level int) { 62 | compressor, _ := flate.NewWriter(dest, level) 63 | compressor.Write(src) 64 | compressor.Close() 65 | } 66 | 67 | // compress uses flate to decompress an io.Reader 68 | func decompress(src io.Reader, dest io.Writer) { 69 | decompressor := flate.NewReader(src) 70 | io.Copy(dest, decompressor) 71 | decompressor.Close() 72 | } 73 | 74 | // http://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang 75 | var src = rand.NewSource(time.Now().UnixNano()) 76 | 77 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 78 | const ( 79 | letterIdxBits = 6 // 6 bits to represent a letter index 80 | letterIdxMask = 1<= 0; { 89 | if remain == 0 { 90 | cache, remain = src.Int63(), letterIdxMax 91 | } 92 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 93 | b[i] = letterBytes[idx] 94 | i-- 95 | } 96 | cache >>= letterIdxBits 97 | remain-- 98 | } 99 | 100 | return string(b) 101 | } 102 | --------------------------------------------------------------------------------