mirror of
https://github.com/schollz/cowyo.git
synced 2023-08-10 21:13:00 +03:00
Vendoring
This commit is contained in:
parent
21047443d6
commit
bef20f3366
243
Gopkg.lock
generated
Normal file
243
Gopkg.lock
generated
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "dmitri.shuralyov.com/kebabcase"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "bf160e40a7918fbe9dc3cc841a023d87242bd2eb"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/boj/redistore"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "fc113767cd6b051980f260d6dbe84b2740c46ab0"
|
||||||
|
version = "v1.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/bradfitz/gomemcache"
|
||||||
|
packages = ["memcache"]
|
||||||
|
revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/bradleypeabody/gorilla-sessions-memcache"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "75ee37df8664a47cd5d9eb84d41e1f2b08086893"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/garyburd/redigo"
|
||||||
|
packages = ["internal","redis"]
|
||||||
|
revision = "433969511232c397de61b1442f9fd49ec06ae9ba"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gin-contrib/multitemplate"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "bbc6daf6024bc4c48f334a0490321cd48a24da3d"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gin-contrib/sessions"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "cccdeef56346e7037ca92de250c2b55ef5e7ffe3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gin-contrib/sse"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/gin-gonic/gin"
|
||||||
|
packages = [".","binding","render"]
|
||||||
|
revision = "d459835d2b077e44f7c9b453505ee29881d5d12d"
|
||||||
|
version = "v1.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/golang/protobuf"
|
||||||
|
packages = ["proto"]
|
||||||
|
revision = "130e6b02ab059e7b717a096f397c5b60111cae74"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/gorilla/context"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||||
|
version = "v1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/gorilla/securecookie"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "667fe4e3466a040b780561fe9b51a83a3753eefc"
|
||||||
|
version = "v1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/gorilla/sessions"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896"
|
||||||
|
version = "v1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/jcelliott/lumber"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "dd349441af25132d146d7095c6693a15431fc9b1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/kidstuff/mongostore"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "256d65ac5b0e35e7c5ebb3f175c0bed1e5c2b253"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||||
|
version = "v0.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/microcosm-cc/bluemonday"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "68fecaef60268522d2ac3f0123cec9d3bcab7b6e"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/russross/blackfriday"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "4048872b16cc0fc2c5fd9eacf0ed2c2fedaa0c8c"
|
||||||
|
version = "v1.5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/schollz/cryptopasta"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "dcd61c7d42a11660da3789311050e961c0a5be55"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/schollz/versionedtext"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "1cef32a305b7272d4df0454533de5e4ba67adfc1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/sergi/go-diff"
|
||||||
|
packages = ["diffmatchpatch"]
|
||||||
|
revision = "feef008d51ad2b3778f85d387ccf91735543008d"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/github_flavored_markdown"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "cccd3ce4f8e394ae9f87de0bd8b37e00625913d9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/go"
|
||||||
|
packages = ["parserutil","printerutil","reflectfind","reflectsource"]
|
||||||
|
revision = "c661e953e604ba4a84a3c4e458462a481bd6ce72"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/go-goon"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "37c2f522c041b74919a9e5e3a6c5c47eb34730a5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/graphql"
|
||||||
|
packages = ["ident"]
|
||||||
|
revision = "d1a3e018e03c4a6b00bc8641eb272872fff371dd"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/highlight_diff"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "09bb4053de1b1d872a9f25dc21378fa71dca4e4e"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/highlight_go"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "78fb10f4a5f89e812a5e26ab723b954a51226086"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/octiconssvg"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "8c9861b86a08c72d14e0285d0dc313bb6df52295"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/sourcegraph/annotate"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "f4cad6c6324d3f584e1743d8b3e0e017a5f3a636"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/sourcegraph/syntaxhighlight"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "bd320f5d308e1a3c4314c678d8227a0d72574ae7"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/ugorji/go"
|
||||||
|
packages = ["codec"]
|
||||||
|
revision = "54210f4e076c57f351166f0ed60e67d3fca57a36"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/crypto"
|
||||||
|
packages = ["bcrypt","blowfish"]
|
||||||
|
revision = "9419663f5a44be8b34ca85f08abc5fe1be11f8a3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/net"
|
||||||
|
packages = ["html","html/atom"]
|
||||||
|
revision = "0a9397675ba34b2845f758fe3cd68828369c6517"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "314a259e304ff91bd6985da2a7149bbf91237993"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "gopkg.in/go-playground/validator.v8"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
|
||||||
|
version = "v8.18.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "v2"
|
||||||
|
name = "gopkg.in/mgo.v2"
|
||||||
|
packages = [".","bson","internal/json","internal/sasl","internal/scram"]
|
||||||
|
revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "gopkg.in/urfave/cli.v1"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||||
|
version = "v1.20.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "v2"
|
||||||
|
name = "gopkg.in/yaml.v2"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "3b488719d6a087407dfd8d956f305228c41a71472ce5c12d09c3a22cdaacbbcb"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
66
Gopkg.toml
Normal file
66
Gopkg.toml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
# Gopkg.toml example
|
||||||
|
#
|
||||||
|
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||||
|
# for detailed Gopkg.toml documentation.
|
||||||
|
#
|
||||||
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
|
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project"
|
||||||
|
# version = "1.0.0"
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project2"
|
||||||
|
# branch = "dev"
|
||||||
|
# source = "github.com/myfork/project2"
|
||||||
|
#
|
||||||
|
# [[override]]
|
||||||
|
# name = "github.com/x/y"
|
||||||
|
# version = "2.4.0"
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gin-contrib/multitemplate"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gin-contrib/sessions"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/gin-gonic/gin"
|
||||||
|
version = "1.2.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/jcelliott/lumber"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/microcosm-cc/bluemonday"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/russross/blackfriday"
|
||||||
|
version = "1.5"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/schollz/cryptopasta"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/schollz/versionedtext"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/shurcooL/github_flavored_markdown"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/crypto"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "gopkg.in/urfave/cli.v1"
|
||||||
|
version = "1.20.0"
|
18
vendor/dmitri.shuralyov.com/kebabcase/kebabcase.go
generated
vendored
Normal file
18
vendor/dmitri.shuralyov.com/kebabcase/kebabcase.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Package kebabcase provides a parser for identifier names
|
||||||
|
// using kebab-case naming convention.
|
||||||
|
//
|
||||||
|
// Reference: https://en.wikipedia.org/wiki/Naming_convention_(programming)#Multiple-word_identifiers.
|
||||||
|
package kebabcase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shurcooL/graphql/ident"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse parses a kebab-case identifier name.
|
||||||
|
//
|
||||||
|
// E.g., "client-mutation-id" -> {"client", "mutation", "id"}.
|
||||||
|
func Parse(name string) ident.Name {
|
||||||
|
return ident.Name(strings.Split(name, "-"))
|
||||||
|
}
|
47
vendor/dmitri.shuralyov.com/kebabcase/kebabcase_test.go
generated
vendored
Normal file
47
vendor/dmitri.shuralyov.com/kebabcase/kebabcase_test.go
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package kebabcase_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dmitri.shuralyov.com/kebabcase"
|
||||||
|
"github.com/shurcooL/graphql/ident"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_kebabCaseToMixedCaps() {
|
||||||
|
fmt.Println(kebabcase.Parse("client-mutation-id").ToMixedCaps())
|
||||||
|
|
||||||
|
// Output: ClientMutationID
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
want ident.Name
|
||||||
|
}{
|
||||||
|
{in: "book", want: ident.Name{"book"}},
|
||||||
|
{in: "bookmark", want: ident.Name{"bookmark"}},
|
||||||
|
{in: "arrow-right", want: ident.Name{"arrow", "right"}},
|
||||||
|
{in: "arrow-small-right", want: ident.Name{"arrow", "small", "right"}},
|
||||||
|
{in: "device-camera-video-audio", want: ident.Name{"device", "camera", "video", "audio"}},
|
||||||
|
{in: "rss", want: ident.Name{"rss"}},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
got := kebabcase.Parse(tc.in)
|
||||||
|
if !reflect.DeepEqual(got, tc.want) {
|
||||||
|
t.Errorf("got: %q, want: %q", got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParse(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
kebabcase.Parse("book")
|
||||||
|
kebabcase.Parse("bookmark")
|
||||||
|
kebabcase.Parse("arrow-right")
|
||||||
|
kebabcase.Parse("arrow-small-right")
|
||||||
|
kebabcase.Parse("device-camera-video-audio")
|
||||||
|
kebabcase.Parse("rss")
|
||||||
|
}
|
||||||
|
}
|
2
vendor/github.com/boj/redistore/.gitignore
generated
vendored
Normal file
2
vendor/github.com/boj/redistore/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
|
19
vendor/github.com/boj/redistore/LICENSE
generated
vendored
Normal file
19
vendor/github.com/boj/redistore/LICENSE
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2013 Brian Jones
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
52
vendor/github.com/boj/redistore/README.md
generated
vendored
Normal file
52
vendor/github.com/boj/redistore/README.md
generated
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# redistore
|
||||||
|
|
||||||
|
[![Build Status](https://drone.io/github.com/boj/redistore/status.png)](https://drone.io/github.com/boj/redistore/latest)
|
||||||
|
|
||||||
|
A session store backend for [gorilla/sessions](http://www.gorillatoolkit.org/pkg/sessions) - [src](https://github.com/gorilla/sessions).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Depends on the [Redigo](https://github.com/garyburd/redigo) Redis library.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
go get gopkg.in/boj/redistore.v1
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Available on [godoc.org](http://www.godoc.org/gopkg.in/boj/redistore.v1).
|
||||||
|
|
||||||
|
See http://www.gorillatoolkit.org/pkg/sessions for full documentation on underlying interface.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
// Fetch new store.
|
||||||
|
store, err := NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
// Get a session.
|
||||||
|
session, err = store.Get(req, "session-key")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a value.
|
||||||
|
session.Values["foo"] = "bar"
|
||||||
|
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete session.
|
||||||
|
session.Options.MaxAge = -1
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change session storage configuration for MaxAge = 10 days.
|
||||||
|
store.SetMaxAge(10*24*3600)
|
||||||
|
|
4
vendor/github.com/boj/redistore/doc.go
generated
vendored
Normal file
4
vendor/github.com/boj/redistore/doc.go
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package redistore is a session store backend for gorilla/sessions
|
||||||
|
*/
|
||||||
|
package redistore
|
358
vendor/github.com/boj/redistore/redistore.go
generated
vendored
Normal file
358
vendor/github.com/boj/redistore/redistore.go
generated
vendored
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
// Copyright 2012 Brian "bojo" Jones. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package redistore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Amount of time for cookies/redis keys to expire.
|
||||||
|
var sessionExpire = 86400 * 30
|
||||||
|
|
||||||
|
// SessionSerializer provides an interface hook for alternative serializers
|
||||||
|
type SessionSerializer interface {
|
||||||
|
Deserialize(d []byte, ss *sessions.Session) error
|
||||||
|
Serialize(ss *sessions.Session) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONSerializer encode the session map to JSON.
|
||||||
|
type JSONSerializer struct{}
|
||||||
|
|
||||||
|
// Serialize to JSON. Will err if there are unmarshalable key values
|
||||||
|
func (s JSONSerializer) Serialize(ss *sessions.Session) ([]byte, error) {
|
||||||
|
m := make(map[string]interface{}, len(ss.Values))
|
||||||
|
for k, v := range ss.Values {
|
||||||
|
ks, ok := k.(string)
|
||||||
|
if !ok {
|
||||||
|
err := fmt.Errorf("Non-string key value, cannot serialize session to JSON: %v", k)
|
||||||
|
fmt.Printf("redistore.JSONSerializer.serialize() Error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m[ks] = v
|
||||||
|
}
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize back to map[string]interface{}
|
||||||
|
func (s JSONSerializer) Deserialize(d []byte, ss *sessions.Session) error {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(d, &m)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("redistore.JSONSerializer.deserialize() Error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
ss.Values[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobSerializer uses gob package to encode the session map
|
||||||
|
type GobSerializer struct{}
|
||||||
|
|
||||||
|
// Serialize using gob
|
||||||
|
func (s GobSerializer) Serialize(ss *sessions.Session) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
enc := gob.NewEncoder(buf)
|
||||||
|
err := enc.Encode(ss.Values)
|
||||||
|
if err == nil {
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize back to map[interface{}]interface{}
|
||||||
|
func (s GobSerializer) Deserialize(d []byte, ss *sessions.Session) error {
|
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(d))
|
||||||
|
return dec.Decode(&ss.Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RediStore stores sessions in a redis backend.
|
||||||
|
type RediStore struct {
|
||||||
|
Pool *redis.Pool
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *sessions.Options // default configuration
|
||||||
|
DefaultMaxAge int // default Redis TTL for a MaxAge == 0 session
|
||||||
|
maxLength int
|
||||||
|
keyPrefix string
|
||||||
|
serializer SessionSerializer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxLength sets RediStore.maxLength if the `l` argument is greater or equal 0
|
||||||
|
// maxLength restricts the maximum length of new sessions to l.
|
||||||
|
// If l is 0 there is no limit to the size of a session, use with caution.
|
||||||
|
// The default for a new RediStore is 4096. Redis allows for max.
|
||||||
|
// value sizes of up to 512MB (http://redis.io/topics/data-types)
|
||||||
|
// Default: 4096,
|
||||||
|
func (s *RediStore) SetMaxLength(l int) {
|
||||||
|
if l >= 0 {
|
||||||
|
s.maxLength = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyPrefix set the prefix
|
||||||
|
func (s *RediStore) SetKeyPrefix(p string) {
|
||||||
|
s.keyPrefix = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSerializer sets the serializer
|
||||||
|
func (s *RediStore) SetSerializer(ss SessionSerializer) {
|
||||||
|
s.serializer = ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxAge restricts the maximum age, in seconds, of the session record
|
||||||
|
// both in database and a browser. This is to change session storage configuration.
|
||||||
|
// If you want just to remove session use your session `s` object and change it's
|
||||||
|
// `Options.MaxAge` to -1, as specified in
|
||||||
|
// http://godoc.org/github.com/gorilla/sessions#Options
|
||||||
|
//
|
||||||
|
// Default is the one provided by this package value - `sessionExpire`.
|
||||||
|
// Set it to 0 for no restriction.
|
||||||
|
// Because we use `MaxAge` also in SecureCookie crypting algorithm you should
|
||||||
|
// use this function to change `MaxAge` value.
|
||||||
|
func (s *RediStore) SetMaxAge(v int) {
|
||||||
|
var c *securecookie.SecureCookie
|
||||||
|
var ok bool
|
||||||
|
s.Options.MaxAge = v
|
||||||
|
for i := range s.Codecs {
|
||||||
|
if c, ok = s.Codecs[i].(*securecookie.SecureCookie); ok {
|
||||||
|
c.MaxAge(v)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Can't change MaxAge on codec %v\n", s.Codecs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dial(network, address, password string) (redis.Conn, error) {
|
||||||
|
c, err := redis.Dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
if _, err := c.Do("AUTH", password); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRediStore returns a new RediStore.
|
||||||
|
// size: maximum number of idle connections.
|
||||||
|
func NewRediStore(size int, network, address, password string, keyPairs ...[]byte) (*RediStore, error) {
|
||||||
|
return NewRediStoreWithPool(&redis.Pool{
|
||||||
|
MaxIdle: size,
|
||||||
|
IdleTimeout: 240 * time.Second,
|
||||||
|
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
_, err := c.Do("PING")
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
Dial: func() (redis.Conn, error) {
|
||||||
|
return dial(network, address, password)
|
||||||
|
},
|
||||||
|
}, keyPairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialWithDB(network, address, password, DB string) (redis.Conn, error) {
|
||||||
|
c, err := dial(network, address, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := c.Do("SELECT", DB); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRediStoreWithDB - like NewRedisStore but accepts `DB` parameter to select
|
||||||
|
// redis DB instead of using the default one ("0")
|
||||||
|
func NewRediStoreWithDB(size int, network, address, password, DB string, keyPairs ...[]byte) (*RediStore, error) {
|
||||||
|
return NewRediStoreWithPool(&redis.Pool{
|
||||||
|
MaxIdle: size,
|
||||||
|
IdleTimeout: 240 * time.Second,
|
||||||
|
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
_, err := c.Do("PING")
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
Dial: func() (redis.Conn, error) {
|
||||||
|
return dialWithDB(network, address, password, DB)
|
||||||
|
},
|
||||||
|
}, keyPairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRediStoreWithPool instantiates a RediStore with a *redis.Pool passed in.
|
||||||
|
func NewRediStoreWithPool(pool *redis.Pool, keyPairs ...[]byte) (*RediStore, error) {
|
||||||
|
rs := &RediStore{
|
||||||
|
// http://godoc.org/github.com/garyburd/redigo/redis#Pool
|
||||||
|
Pool: pool,
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||||
|
Options: &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: sessionExpire,
|
||||||
|
},
|
||||||
|
DefaultMaxAge: 60 * 20, // 20 minutes seems like a reasonable default
|
||||||
|
maxLength: 4096,
|
||||||
|
keyPrefix: "session_",
|
||||||
|
serializer: GobSerializer{},
|
||||||
|
}
|
||||||
|
_, err := rs.ping()
|
||||||
|
return rs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying *redis.Pool
|
||||||
|
func (s *RediStore) Close() error {
|
||||||
|
return s.Pool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// See gorilla/sessions FilesystemStore.Get().
|
||||||
|
func (s *RediStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
return sessions.GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// See gorilla/sessions FilesystemStore.New().
|
||||||
|
func (s *RediStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
var err error
|
||||||
|
session := sessions.NewSession(s, name)
|
||||||
|
// make a copy
|
||||||
|
options := *s.Options
|
||||||
|
session.Options = &options
|
||||||
|
session.IsNew = true
|
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
ok, err := s.load(session)
|
||||||
|
session.IsNew = !(err == nil && ok) // not new if no error and data available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||||
|
// Marked for deletion.
|
||||||
|
if session.Options.MaxAge < 0 {
|
||||||
|
if err := s.delete(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
|
||||||
|
} else {
|
||||||
|
// Build an alphanumeric key for the redis store.
|
||||||
|
if session.ID == "" {
|
||||||
|
session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=")
|
||||||
|
}
|
||||||
|
if err := s.save(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the session from redis, and sets the cookie to expire.
|
||||||
|
//
|
||||||
|
// WARNING: This method should be considered deprecated since it is not exposed via the gorilla/sessions interface.
|
||||||
|
// Set session.Options.MaxAge = -1 and call Save instead. - July 18th, 2013
|
||||||
|
func (s *RediStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||||
|
conn := s.Pool.Get()
|
||||||
|
defer conn.Close()
|
||||||
|
if _, err := conn.Do("DEL", s.keyPrefix+session.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Set cookie to expire.
|
||||||
|
options := *session.Options
|
||||||
|
options.MaxAge = -1
|
||||||
|
http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options))
|
||||||
|
// Clear session values.
|
||||||
|
for k := range session.Values {
|
||||||
|
delete(session.Values, k)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ping does an internal ping against a server to check if it is alive.
|
||||||
|
func (s *RediStore) ping() (bool, error) {
|
||||||
|
conn := s.Pool.Get()
|
||||||
|
defer conn.Close()
|
||||||
|
data, err := conn.Do("PING")
|
||||||
|
if err != nil || data == nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return (data == "PONG"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save stores the session in redis.
|
||||||
|
func (s *RediStore) save(session *sessions.Session) error {
|
||||||
|
b, err := s.serializer.Serialize(session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.maxLength != 0 && len(b) > s.maxLength {
|
||||||
|
return errors.New("SessionStore: the value to store is too big")
|
||||||
|
}
|
||||||
|
conn := s.Pool.Get()
|
||||||
|
defer conn.Close()
|
||||||
|
if err = conn.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
age := session.Options.MaxAge
|
||||||
|
if age == 0 {
|
||||||
|
age = s.DefaultMaxAge
|
||||||
|
}
|
||||||
|
_, err = conn.Do("SETEX", s.keyPrefix+session.ID, age, b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// load reads the session from redis.
|
||||||
|
// returns true if there is a sessoin data in DB
|
||||||
|
func (s *RediStore) load(session *sessions.Session) (bool, error) {
|
||||||
|
conn := s.Pool.Get()
|
||||||
|
defer conn.Close()
|
||||||
|
if err := conn.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
data, err := conn.Do("GET", s.keyPrefix+session.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
return false, nil // no data was associated with this key
|
||||||
|
}
|
||||||
|
b, err := redis.Bytes(data, err)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, s.serializer.Deserialize(b, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete removes keys from redis if MaxAge<0
|
||||||
|
func (s *RediStore) delete(session *sessions.Session) error {
|
||||||
|
conn := s.Pool.Get()
|
||||||
|
defer conn.Close()
|
||||||
|
if _, err := conn.Do("DEL", s.keyPrefix+session.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
404
vendor/github.com/boj/redistore/redistore_test.go
generated
vendored
Normal file
404
vendor/github.com/boj/redistore/redistore_test.go
generated
vendored
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
package redistore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/gob"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// ResponseRecorder
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// ResponseRecorder is an implementation of http.ResponseWriter that
|
||||||
|
// records its mutations for later inspection in tests.
|
||||||
|
type ResponseRecorder struct {
|
||||||
|
Code int // the HTTP response code from WriteHeader
|
||||||
|
HeaderMap http.Header // the HTTP response headers
|
||||||
|
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
|
||||||
|
Flushed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRecorder returns an initialized ResponseRecorder.
|
||||||
|
func NewRecorder() *ResponseRecorder {
|
||||||
|
return &ResponseRecorder{
|
||||||
|
HeaderMap: make(http.Header),
|
||||||
|
Body: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
|
||||||
|
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
|
||||||
|
const DefaultRemoteAddr = "1.2.3.4"
|
||||||
|
|
||||||
|
// Header returns the response headers.
|
||||||
|
func (rw *ResponseRecorder) Header() http.Header {
|
||||||
|
return rw.HeaderMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write always succeeds and writes to rw.Body, if not nil.
|
||||||
|
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
|
||||||
|
if rw.Body != nil {
|
||||||
|
rw.Body.Write(buf)
|
||||||
|
}
|
||||||
|
if rw.Code == 0 {
|
||||||
|
rw.Code = http.StatusOK
|
||||||
|
}
|
||||||
|
return len(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader sets rw.Code.
|
||||||
|
func (rw *ResponseRecorder) WriteHeader(code int) {
|
||||||
|
rw.Code = code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush sets rw.Flushed to true.
|
||||||
|
func (rw *ResponseRecorder) Flush() {
|
||||||
|
rw.Flushed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type FlashMessage struct {
|
||||||
|
Type int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRediStore(t *testing.T) {
|
||||||
|
var req *http.Request
|
||||||
|
var rsp *ResponseRecorder
|
||||||
|
var hdr http.Header
|
||||||
|
var err error
|
||||||
|
var ok bool
|
||||||
|
var cookies []string
|
||||||
|
var session *sessions.Session
|
||||||
|
var flashes []interface{}
|
||||||
|
|
||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Round 1 ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// RedisStore
|
||||||
|
store, err := NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
rsp = NewRecorder()
|
||||||
|
// Get a session.
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Get a flash.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 0 {
|
||||||
|
t.Errorf("Expected empty flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
// Add some flashes.
|
||||||
|
session.AddFlash("foo")
|
||||||
|
session.AddFlash("bar")
|
||||||
|
// Custom key.
|
||||||
|
session.AddFlash("baz", "custom_key")
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
hdr = rsp.Header()
|
||||||
|
cookies, ok = hdr["Set-Cookie"]
|
||||||
|
if !ok || len(cookies) != 1 {
|
||||||
|
t.Fatalf("No cookies. Header:", hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 2 ----------------------------------------------------------------
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
req.Header.Add("Cookie", cookies[0])
|
||||||
|
rsp = NewRecorder()
|
||||||
|
// Get a session.
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Check all saved values.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 2 {
|
||||||
|
t.Fatalf("Expected flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
if flashes[0] != "foo" || flashes[1] != "bar" {
|
||||||
|
t.Errorf("Expected foo,bar; Got %v", flashes)
|
||||||
|
}
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 0 {
|
||||||
|
t.Errorf("Expected dumped flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
// Custom key.
|
||||||
|
flashes = session.Flashes("custom_key")
|
||||||
|
if len(flashes) != 1 {
|
||||||
|
t.Errorf("Expected flashes; Got %v", flashes)
|
||||||
|
} else if flashes[0] != "baz" {
|
||||||
|
t.Errorf("Expected baz; Got %v", flashes)
|
||||||
|
}
|
||||||
|
flashes = session.Flashes("custom_key")
|
||||||
|
if len(flashes) != 0 {
|
||||||
|
t.Errorf("Expected dumped flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RediStore specific
|
||||||
|
// Set MaxAge to -1 to mark for deletion.
|
||||||
|
session.Options.MaxAge = -1
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 3 ----------------------------------------------------------------
|
||||||
|
// Custom type
|
||||||
|
|
||||||
|
// RedisStore
|
||||||
|
store, err = NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
rsp = NewRecorder()
|
||||||
|
// Get a session.
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Get a flash.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 0 {
|
||||||
|
t.Errorf("Expected empty flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
// Add some flashes.
|
||||||
|
session.AddFlash(&FlashMessage{42, "foo"})
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
hdr = rsp.Header()
|
||||||
|
cookies, ok = hdr["Set-Cookie"]
|
||||||
|
if !ok || len(cookies) != 1 {
|
||||||
|
t.Fatalf("No cookies. Header:", hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 4 ----------------------------------------------------------------
|
||||||
|
// Custom type
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
req.Header.Add("Cookie", cookies[0])
|
||||||
|
rsp = NewRecorder()
|
||||||
|
// Get a session.
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Check all saved values.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 1 {
|
||||||
|
t.Fatalf("Expected flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
custom := flashes[0].(FlashMessage)
|
||||||
|
if custom.Type != 42 || custom.Message != "foo" {
|
||||||
|
t.Errorf("Expected %#v, got %#v", FlashMessage{42, "foo"}, custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RediStore specific
|
||||||
|
// Set MaxAge to -1 to mark for deletion.
|
||||||
|
session.Options.MaxAge = -1
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 5 ----------------------------------------------------------------
|
||||||
|
// RediStore Delete session (deprecated)
|
||||||
|
|
||||||
|
//req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
//req.Header.Add("Cookie", cookies[0])
|
||||||
|
//rsp = NewRecorder()
|
||||||
|
//// Get a session.
|
||||||
|
//if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
// t.Fatalf("Error getting session: %v", err)
|
||||||
|
//}
|
||||||
|
//// Delete session.
|
||||||
|
//if err = store.Delete(req, rsp, session); err != nil {
|
||||||
|
// t.Fatalf("Error deleting session: %v", err)
|
||||||
|
//}
|
||||||
|
//// Get a flash.
|
||||||
|
//flashes = session.Flashes()
|
||||||
|
//if len(flashes) != 0 {
|
||||||
|
// t.Errorf("Expected empty flashes; Got %v", flashes)
|
||||||
|
//}
|
||||||
|
//hdr = rsp.Header()
|
||||||
|
//cookies, ok = hdr["Set-Cookie"]
|
||||||
|
//if !ok || len(cookies) != 1 {
|
||||||
|
// t.Fatalf("No cookies. Header:", hdr)
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Round 6 ----------------------------------------------------------------
|
||||||
|
// RediStore change MaxLength of session
|
||||||
|
|
||||||
|
store, err = NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest("GET", "http://www.example.com", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to create request", err)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
session, err = store.New(req, "my session")
|
||||||
|
session.Values["big"] = make([]byte, base64.StdEncoding.DecodedLen(4096*2))
|
||||||
|
err = session.Save(req, w)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
store.SetMaxLength(4096 * 3) // A bit more than the value size to account for encoding overhead.
|
||||||
|
err = session.Save(req, w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to Save:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 7 ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// RedisStoreWithDB
|
||||||
|
store, err = NewRediStoreWithDB(10, "tcp", ":6379", "", "1", []byte("secret-key"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
rsp = NewRecorder()
|
||||||
|
// Get a session. Using the same key as previously, but on different DB
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Get a flash.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 0 {
|
||||||
|
t.Errorf("Expected empty flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
// Add some flashes.
|
||||||
|
session.AddFlash("foo")
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
hdr = rsp.Header()
|
||||||
|
cookies, ok = hdr["Set-Cookie"]
|
||||||
|
if !ok || len(cookies) != 1 {
|
||||||
|
t.Fatalf("No cookies. Header:", hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a session.
|
||||||
|
req.Header.Add("Cookie", cookies[0])
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Check all saved values.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 1 {
|
||||||
|
t.Fatalf("Expected flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
if flashes[0] != "foo" {
|
||||||
|
t.Errorf("Expected foo,bar; Got %v", flashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 8 ----------------------------------------------------------------
|
||||||
|
// JSONSerializer
|
||||||
|
|
||||||
|
// RedisStore
|
||||||
|
store, err = NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
store.SetSerializer(JSONSerializer{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||||
|
rsp = NewRecorder()
|
||||||
|
// Get a session.
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Get a flash.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 0 {
|
||||||
|
t.Errorf("Expected empty flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
// Add some flashes.
|
||||||
|
session.AddFlash("foo")
|
||||||
|
// Save.
|
||||||
|
if err = sessions.Save(req, rsp); err != nil {
|
||||||
|
t.Fatalf("Error saving session: %v", err)
|
||||||
|
}
|
||||||
|
hdr = rsp.Header()
|
||||||
|
cookies, ok = hdr["Set-Cookie"]
|
||||||
|
if !ok || len(cookies) != 1 {
|
||||||
|
t.Fatalf("No cookies. Header:", hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a session.
|
||||||
|
req.Header.Add("Cookie", cookies[0])
|
||||||
|
if session, err = store.Get(req, "session-key"); err != nil {
|
||||||
|
t.Fatalf("Error getting session: %v", err)
|
||||||
|
}
|
||||||
|
// Check all saved values.
|
||||||
|
flashes = session.Flashes()
|
||||||
|
if len(flashes) != 1 {
|
||||||
|
t.Fatalf("Expected flashes; Got %v", flashes)
|
||||||
|
}
|
||||||
|
if flashes[0] != "foo" {
|
||||||
|
t.Errorf("Expected foo,bar; Got %v", flashes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPingGoodPort(t *testing.T) {
|
||||||
|
store, _ := NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
defer store.Close()
|
||||||
|
ok, err := store.ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected server to PONG")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPingBadPort(t *testing.T) {
|
||||||
|
store, _ := NewRediStore(10, "tcp", ":6378", "", []byte("secret-key"))
|
||||||
|
defer store.Close()
|
||||||
|
_, err := store.ping()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRediStore() {
|
||||||
|
// RedisStore
|
||||||
|
store, err := NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(FlashMessage{})
|
||||||
|
}
|
3
vendor/github.com/bradfitz/gomemcache/.gitignore
generated
vendored
Normal file
3
vendor/github.com/bradfitz/gomemcache/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
_*
|
||||||
|
*.out
|
||||||
|
*~
|
202
vendor/github.com/bradfitz/gomemcache/LICENSE
generated
vendored
Normal file
202
vendor/github.com/bradfitz/gomemcache/LICENSE
generated
vendored
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
37
vendor/github.com/bradfitz/gomemcache/README.md
generated
vendored
Normal file
37
vendor/github.com/bradfitz/gomemcache/README.md
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
## About
|
||||||
|
|
||||||
|
This is a memcache client library for the Go programming language
|
||||||
|
(http://golang.org/).
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
### Using *go get*
|
||||||
|
|
||||||
|
$ go get github.com/bradfitz/gomemcache/memcache
|
||||||
|
|
||||||
|
After this command *gomemcache* is ready to use. Its source will be in:
|
||||||
|
|
||||||
|
$GOPATH/src/github.com/bradfitz/gomemcache/memcache
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mc := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
|
||||||
|
mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})
|
||||||
|
|
||||||
|
it, err := mc.Get("foo")
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
## Full docs, see:
|
||||||
|
|
||||||
|
See https://godoc.org/github.com/bradfitz/gomemcache/memcache
|
||||||
|
|
||||||
|
Or run:
|
||||||
|
|
||||||
|
$ godoc github.com/bradfitz/gomemcache/memcache
|
||||||
|
|
684
vendor/github.com/bradfitz/gomemcache/memcache/memcache.go
generated
vendored
Normal file
684
vendor/github.com/bradfitz/gomemcache/memcache/memcache.go
generated
vendored
Normal file
@ -0,0 +1,684 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2011 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package memcache provides a client for the memcached cache server.
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Similar to:
|
||||||
|
// http://code.google.com/appengine/docs/go/memcache/reference.html
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrCacheMiss means that a Get failed because the item wasn't present.
|
||||||
|
ErrCacheMiss = errors.New("memcache: cache miss")
|
||||||
|
|
||||||
|
// ErrCASConflict means that a CompareAndSwap call failed due to the
|
||||||
|
// cached value being modified between the Get and the CompareAndSwap.
|
||||||
|
// If the cached value was simply evicted rather than replaced,
|
||||||
|
// ErrNotStored will be returned instead.
|
||||||
|
ErrCASConflict = errors.New("memcache: compare-and-swap conflict")
|
||||||
|
|
||||||
|
// ErrNotStored means that a conditional write operation (i.e. Add or
|
||||||
|
// CompareAndSwap) failed because the condition was not satisfied.
|
||||||
|
ErrNotStored = errors.New("memcache: item not stored")
|
||||||
|
|
||||||
|
// ErrServer means that a server error occurred.
|
||||||
|
ErrServerError = errors.New("memcache: server error")
|
||||||
|
|
||||||
|
// ErrNoStats means that no statistics were available.
|
||||||
|
ErrNoStats = errors.New("memcache: no statistics available")
|
||||||
|
|
||||||
|
// ErrMalformedKey is returned when an invalid key is used.
|
||||||
|
// Keys must be at maximum 250 bytes long and not
|
||||||
|
// contain whitespace or control characters.
|
||||||
|
ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters")
|
||||||
|
|
||||||
|
// ErrNoServers is returned when no servers are configured or available.
|
||||||
|
ErrNoServers = errors.New("memcache: no servers configured or available")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultTimeout is the default socket read/write timeout.
|
||||||
|
DefaultTimeout = 100 * time.Millisecond
|
||||||
|
|
||||||
|
// DefaultMaxIdleConns is the default maximum number of idle connections
|
||||||
|
// kept for any single address.
|
||||||
|
DefaultMaxIdleConns = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const buffered = 8 // arbitrary buffered channel size, for readability
|
||||||
|
|
||||||
|
// resumableError returns true if err is only a protocol-level cache error.
|
||||||
|
// This is used to determine whether or not a server connection should
|
||||||
|
// be re-used or not. If an error occurs, by default we don't reuse the
|
||||||
|
// connection, unless it was just a cache error.
|
||||||
|
func resumableError(err error) bool {
|
||||||
|
switch err {
|
||||||
|
case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func legalKey(key string) bool {
|
||||||
|
if len(key) > 250 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(key); i++ {
|
||||||
|
if key[i] <= ' ' || key[i] == 0x7f {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
crlf = []byte("\r\n")
|
||||||
|
space = []byte(" ")
|
||||||
|
resultOK = []byte("OK\r\n")
|
||||||
|
resultStored = []byte("STORED\r\n")
|
||||||
|
resultNotStored = []byte("NOT_STORED\r\n")
|
||||||
|
resultExists = []byte("EXISTS\r\n")
|
||||||
|
resultNotFound = []byte("NOT_FOUND\r\n")
|
||||||
|
resultDeleted = []byte("DELETED\r\n")
|
||||||
|
resultEnd = []byte("END\r\n")
|
||||||
|
resultOk = []byte("OK\r\n")
|
||||||
|
resultTouched = []byte("TOUCHED\r\n")
|
||||||
|
|
||||||
|
resultClientErrorPrefix = []byte("CLIENT_ERROR ")
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a memcache client using the provided server(s)
|
||||||
|
// with equal weight. If a server is listed multiple times,
|
||||||
|
// it gets a proportional amount of weight.
|
||||||
|
func New(server ...string) *Client {
|
||||||
|
ss := new(ServerList)
|
||||||
|
ss.SetServers(server...)
|
||||||
|
return NewFromSelector(ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromSelector returns a new Client using the provided ServerSelector.
|
||||||
|
func NewFromSelector(ss ServerSelector) *Client {
|
||||||
|
return &Client{selector: ss}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a memcache client.
|
||||||
|
// It is safe for unlocked use by multiple concurrent goroutines.
|
||||||
|
type Client struct {
|
||||||
|
// Timeout specifies the socket read/write timeout.
|
||||||
|
// If zero, DefaultTimeout is used.
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// MaxIdleConns specifies the maximum number of idle connections that will
|
||||||
|
// be maintained per address. If less than one, DefaultMaxIdleConns will be
|
||||||
|
// used.
|
||||||
|
//
|
||||||
|
// Consider your expected traffic rates and latency carefully. This should
|
||||||
|
// be set to a number higher than your peak parallel requests.
|
||||||
|
MaxIdleConns int
|
||||||
|
|
||||||
|
selector ServerSelector
|
||||||
|
|
||||||
|
lk sync.Mutex
|
||||||
|
freeconn map[string][]*conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item is an item to be got or stored in a memcached server.
|
||||||
|
type Item struct {
|
||||||
|
// Key is the Item's key (250 bytes maximum).
|
||||||
|
Key string
|
||||||
|
|
||||||
|
// Value is the Item's value.
|
||||||
|
Value []byte
|
||||||
|
|
||||||
|
// Flags are server-opaque flags whose semantics are entirely
|
||||||
|
// up to the app.
|
||||||
|
Flags uint32
|
||||||
|
|
||||||
|
// Expiration is the cache expiration time, in seconds: either a relative
|
||||||
|
// time from now (up to 1 month), or an absolute Unix epoch time.
|
||||||
|
// Zero means the Item has no expiration time.
|
||||||
|
Expiration int32
|
||||||
|
|
||||||
|
// Compare and swap ID.
|
||||||
|
casid uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// conn is a connection to a server.
|
||||||
|
type conn struct {
|
||||||
|
nc net.Conn
|
||||||
|
rw *bufio.ReadWriter
|
||||||
|
addr net.Addr
|
||||||
|
c *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// release returns this connection back to the client's free pool
|
||||||
|
func (cn *conn) release() {
|
||||||
|
cn.c.putFreeConn(cn.addr, cn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cn *conn) extendDeadline() {
|
||||||
|
cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// condRelease releases this connection if the error pointed to by err
|
||||||
|
// is nil (not an error) or is only a protocol level error (e.g. a
|
||||||
|
// cache miss). The purpose is to not recycle TCP connections that
|
||||||
|
// are bad.
|
||||||
|
func (cn *conn) condRelease(err *error) {
|
||||||
|
if *err == nil || resumableError(*err) {
|
||||||
|
cn.release()
|
||||||
|
} else {
|
||||||
|
cn.nc.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) putFreeConn(addr net.Addr, cn *conn) {
|
||||||
|
c.lk.Lock()
|
||||||
|
defer c.lk.Unlock()
|
||||||
|
if c.freeconn == nil {
|
||||||
|
c.freeconn = make(map[string][]*conn)
|
||||||
|
}
|
||||||
|
freelist := c.freeconn[addr.String()]
|
||||||
|
if len(freelist) >= c.maxIdleConns() {
|
||||||
|
cn.nc.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.freeconn[addr.String()] = append(freelist, cn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getFreeConn(addr net.Addr) (cn *conn, ok bool) {
|
||||||
|
c.lk.Lock()
|
||||||
|
defer c.lk.Unlock()
|
||||||
|
if c.freeconn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
freelist, ok := c.freeconn[addr.String()]
|
||||||
|
if !ok || len(freelist) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
cn = freelist[len(freelist)-1]
|
||||||
|
c.freeconn[addr.String()] = freelist[:len(freelist)-1]
|
||||||
|
return cn, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) netTimeout() time.Duration {
|
||||||
|
if c.Timeout != 0 {
|
||||||
|
return c.Timeout
|
||||||
|
}
|
||||||
|
return DefaultTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) maxIdleConns() int {
|
||||||
|
if c.MaxIdleConns > 0 {
|
||||||
|
return c.MaxIdleConns
|
||||||
|
}
|
||||||
|
return DefaultMaxIdleConns
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectTimeoutError is the error type used when it takes
|
||||||
|
// too long to connect to the desired host. This level of
|
||||||
|
// detail can generally be ignored.
|
||||||
|
type ConnectTimeoutError struct {
|
||||||
|
Addr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cte *ConnectTimeoutError) Error() string {
|
||||||
|
return "memcache: connect timeout to " + cte.Addr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dial(addr net.Addr) (net.Conn, error) {
|
||||||
|
type connError struct {
|
||||||
|
cn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
nc, err := net.DialTimeout(addr.Network(), addr.String(), c.netTimeout())
|
||||||
|
if err == nil {
|
||||||
|
return nc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
|
return nil, &ConnectTimeoutError{addr}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getConn(addr net.Addr) (*conn, error) {
|
||||||
|
cn, ok := c.getFreeConn(addr)
|
||||||
|
if ok {
|
||||||
|
cn.extendDeadline()
|
||||||
|
return cn, nil
|
||||||
|
}
|
||||||
|
nc, err := c.dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cn = &conn{
|
||||||
|
nc: nc,
|
||||||
|
addr: addr,
|
||||||
|
rw: bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)),
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
cn.extendDeadline()
|
||||||
|
return cn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error {
|
||||||
|
addr, err := c.selector.PickServer(item.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cn, err := c.getConn(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cn.condRelease(&err)
|
||||||
|
if err = fn(c, cn.rw, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) FlushAll() error {
|
||||||
|
return c.selector.Each(c.flushAllFromAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets the item for the given key. ErrCacheMiss is returned for a
|
||||||
|
// memcache cache miss. The key must be at most 250 bytes in length.
|
||||||
|
func (c *Client) Get(key string) (item *Item, err error) {
|
||||||
|
err = c.withKeyAddr(key, func(addr net.Addr) error {
|
||||||
|
return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it })
|
||||||
|
})
|
||||||
|
if err == nil && item == nil {
|
||||||
|
err = ErrCacheMiss
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch updates the expiry for the given key. The seconds parameter is either
|
||||||
|
// a Unix timestamp or, if seconds is less than 1 month, the number of seconds
|
||||||
|
// into the future at which time the item will expire. ErrCacheMiss is returned if the
|
||||||
|
// key is not in the cache. The key must be at most 250 bytes in length.
|
||||||
|
func (c *Client) Touch(key string, seconds int32) (err error) {
|
||||||
|
return c.withKeyAddr(key, func(addr net.Addr) error {
|
||||||
|
return c.touchFromAddr(addr, []string{key}, seconds)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) withKeyAddr(key string, fn func(net.Addr) error) (err error) {
|
||||||
|
if !legalKey(key) {
|
||||||
|
return ErrMalformedKey
|
||||||
|
}
|
||||||
|
addr, err := c.selector.PickServer(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fn(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) {
|
||||||
|
cn, err := c.getConn(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cn.condRelease(&err)
|
||||||
|
return fn(cn.rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error {
|
||||||
|
return c.withKeyAddr(key, func(addr net.Addr) error {
|
||||||
|
return c.withAddrRw(addr, fn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error {
|
||||||
|
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
|
||||||
|
if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := parseGetResponse(rw.Reader, cb); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushAllFromAddr send the flush_all command to the given addr
|
||||||
|
func (c *Client) flushAllFromAddr(addr net.Addr) error {
|
||||||
|
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
|
||||||
|
if _, err := fmt.Fprintf(rw, "flush_all\r\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultOk):
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("memcache: unexpected response line from flush_all: %q", string(line))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) error {
|
||||||
|
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
|
||||||
|
for _, key := range keys {
|
||||||
|
if _, err := fmt.Fprintf(rw, "touch %s %d\r\n", key, expiration); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultTouched):
|
||||||
|
break
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("memcache: unexpected response line from touch: %q", string(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMulti is a batch version of Get. The returned map from keys to
|
||||||
|
// items may have fewer elements than the input slice, due to memcache
|
||||||
|
// cache misses. Each key must be at most 250 bytes in length.
|
||||||
|
// If no error is returned, the returned map will also be non-nil.
|
||||||
|
func (c *Client) GetMulti(keys []string) (map[string]*Item, error) {
|
||||||
|
var lk sync.Mutex
|
||||||
|
m := make(map[string]*Item)
|
||||||
|
addItemToMap := func(it *Item) {
|
||||||
|
lk.Lock()
|
||||||
|
defer lk.Unlock()
|
||||||
|
m[it.Key] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMap := make(map[net.Addr][]string)
|
||||||
|
for _, key := range keys {
|
||||||
|
if !legalKey(key) {
|
||||||
|
return nil, ErrMalformedKey
|
||||||
|
}
|
||||||
|
addr, err := c.selector.PickServer(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyMap[addr] = append(keyMap[addr], key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan error, buffered)
|
||||||
|
for addr, keys := range keyMap {
|
||||||
|
go func(addr net.Addr, keys []string) {
|
||||||
|
ch <- c.getFromAddr(addr, keys, addItemToMap)
|
||||||
|
}(addr, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _ = range keyMap {
|
||||||
|
if ge := <-ch; ge != nil {
|
||||||
|
err = ge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGetResponse reads a GET response from r and calls cb for each
|
||||||
|
// read and allocated Item
|
||||||
|
func parseGetResponse(r *bufio.Reader, cb func(*Item)) error {
|
||||||
|
for {
|
||||||
|
line, err := r.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Equal(line, resultEnd) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
it := new(Item)
|
||||||
|
size, err := scanGetResponseLine(line, it)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
it.Value, err = ioutil.ReadAll(io.LimitReader(r, int64(size)+2))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.HasSuffix(it.Value, crlf) {
|
||||||
|
return fmt.Errorf("memcache: corrupt get result read")
|
||||||
|
}
|
||||||
|
it.Value = it.Value[:size]
|
||||||
|
cb(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanGetResponseLine populates it and returns the declared size of the item.
|
||||||
|
// It does not read the bytes of the item.
|
||||||
|
func scanGetResponseLine(line []byte, it *Item) (size int, err error) {
|
||||||
|
pattern := "VALUE %s %d %d %d\r\n"
|
||||||
|
dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid}
|
||||||
|
if bytes.Count(line, space) == 3 {
|
||||||
|
pattern = "VALUE %s %d %d\r\n"
|
||||||
|
dest = dest[:3]
|
||||||
|
}
|
||||||
|
n, err := fmt.Sscanf(string(line), pattern, dest...)
|
||||||
|
if err != nil || n != len(dest) {
|
||||||
|
return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line)
|
||||||
|
}
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set writes the given item, unconditionally.
|
||||||
|
func (c *Client) Set(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).set)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) set(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "set", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add writes the given item, if no value already exists for its
|
||||||
|
// key. ErrNotStored is returned if that condition is not met.
|
||||||
|
func (c *Client) Add(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).add)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) add(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "add", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace writes the given item, but only if the server *does*
|
||||||
|
// already hold data for this key
|
||||||
|
func (c *Client) Replace(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) replace(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "replace", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareAndSwap writes the given item that was previously returned
|
||||||
|
// by Get, if the value was neither modified or evicted between the
|
||||||
|
// Get and the CompareAndSwap calls. The item's Key should not change
|
||||||
|
// between calls but all other item fields may differ. ErrCASConflict
|
||||||
|
// is returned if the value was modified in between the
|
||||||
|
// calls. ErrNotStored is returned if the value was evicted in between
|
||||||
|
// the calls.
|
||||||
|
func (c *Client) CompareAndSwap(item *Item) error {
|
||||||
|
return c.onItem(item, (*Client).cas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) cas(rw *bufio.ReadWriter, item *Item) error {
|
||||||
|
return c.populateOne(rw, "cas", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) populateOne(rw *bufio.ReadWriter, verb string, item *Item) error {
|
||||||
|
if !legalKey(item.Key) {
|
||||||
|
return ErrMalformedKey
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if verb == "cas" {
|
||||||
|
_, err = fmt.Fprintf(rw, "%s %s %d %d %d %d\r\n",
|
||||||
|
verb, item.Key, item.Flags, item.Expiration, len(item.Value), item.casid)
|
||||||
|
} else {
|
||||||
|
_, err = fmt.Fprintf(rw, "%s %s %d %d %d\r\n",
|
||||||
|
verb, item.Key, item.Flags, item.Expiration, len(item.Value))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = rw.Write(item.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := rw.Write(crlf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultStored):
|
||||||
|
return nil
|
||||||
|
case bytes.Equal(line, resultNotStored):
|
||||||
|
return ErrNotStored
|
||||||
|
case bytes.Equal(line, resultExists):
|
||||||
|
return ErrCASConflict
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
}
|
||||||
|
return fmt.Errorf("memcache: unexpected response line from %q: %q", verb, string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) {
|
||||||
|
_, err := fmt.Fprintf(rw, format, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rw.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
line, err := rw.ReadSlice('\n')
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error {
|
||||||
|
line, err := writeReadLine(rw, format, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultOK):
|
||||||
|
return nil
|
||||||
|
case bytes.Equal(line, expect):
|
||||||
|
return nil
|
||||||
|
case bytes.Equal(line, resultNotStored):
|
||||||
|
return ErrNotStored
|
||||||
|
case bytes.Equal(line, resultExists):
|
||||||
|
return ErrCASConflict
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
}
|
||||||
|
return fmt.Errorf("memcache: unexpected response line: %q", string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the item with the provided key. The error ErrCacheMiss is
|
||||||
|
// returned if the item didn't already exist in the cache.
|
||||||
|
func (c *Client) Delete(key string) error {
|
||||||
|
return c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
|
||||||
|
return writeExpectf(rw, resultDeleted, "delete %s\r\n", key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAll deletes all items in the cache.
|
||||||
|
func (c *Client) DeleteAll() error {
|
||||||
|
return c.withKeyRw("", func(rw *bufio.ReadWriter) error {
|
||||||
|
return writeExpectf(rw, resultDeleted, "flush_all\r\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment atomically increments key by delta. The return value is
|
||||||
|
// the new value after being incremented or an error. If the value
|
||||||
|
// didn't exist in memcached the error is ErrCacheMiss. The value in
|
||||||
|
// memcached must be an decimal number, or an error will be returned.
|
||||||
|
// On 64-bit overflow, the new value wraps around.
|
||||||
|
func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) {
|
||||||
|
return c.incrDecr("incr", key, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement atomically decrements key by delta. The return value is
|
||||||
|
// the new value after being decremented or an error. If the value
|
||||||
|
// didn't exist in memcached the error is ErrCacheMiss. The value in
|
||||||
|
// memcached must be an decimal number, or an error will be returned.
|
||||||
|
// On underflow, the new value is capped at zero and does not wrap
|
||||||
|
// around.
|
||||||
|
func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) {
|
||||||
|
return c.incrDecr("decr", key, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) incrDecr(verb, key string, delta uint64) (uint64, error) {
|
||||||
|
var val uint64
|
||||||
|
err := c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
|
||||||
|
line, err := writeReadLine(rw, "%s %s %d\r\n", verb, key, delta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(line, resultNotFound):
|
||||||
|
return ErrCacheMiss
|
||||||
|
case bytes.HasPrefix(line, resultClientErrorPrefix):
|
||||||
|
errMsg := line[len(resultClientErrorPrefix) : len(line)-2]
|
||||||
|
return errors.New("memcache: client error: " + string(errMsg))
|
||||||
|
}
|
||||||
|
val, err = strconv.ParseUint(string(line[:len(line)-2]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return val, err
|
||||||
|
}
|
289
vendor/github.com/bradfitz/gomemcache/memcache/memcache_test.go
generated
vendored
Normal file
289
vendor/github.com/bradfitz/gomemcache/memcache/memcache_test.go
generated
vendored
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2011 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package memcache provides a client for the memcached cache server.
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testServer = "localhost:11211"
|
||||||
|
|
||||||
|
func setup(t *testing.T) bool {
|
||||||
|
c, err := net.Dial("tcp", testServer)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping test; no server running at %s", testServer)
|
||||||
|
}
|
||||||
|
c.Write([]byte("flush_all\r\n"))
|
||||||
|
c.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalhost(t *testing.T) {
|
||||||
|
if !setup(t) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testWithClient(t, New(testServer))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the memcached binary as a child process and connect to its unix socket.
|
||||||
|
func TestUnixSocket(t *testing.T) {
|
||||||
|
sock := fmt.Sprintf("/tmp/test-gomemcache-%d.sock", os.Getpid())
|
||||||
|
cmd := exec.Command("memcached", "-s", sock)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
t.Skipf("skipping test; couldn't find memcached")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cmd.Wait()
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
// Wait a bit for the socket to appear.
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if _, err := os.Stat(sock); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(25*i) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
testWithClient(t, New(sock))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustSetF(t *testing.T, c *Client) func(*Item) {
|
||||||
|
return func(it *Item) {
|
||||||
|
if err := c.Set(it); err != nil {
|
||||||
|
t.Fatalf("failed to Set %#v: %v", *it, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithClient(t *testing.T, c *Client) {
|
||||||
|
checkErr := func(err error, format string, args ...interface{}) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustSet := mustSetF(t, c)
|
||||||
|
|
||||||
|
// Set
|
||||||
|
foo := &Item{Key: "foo", Value: []byte("fooval"), Flags: 123}
|
||||||
|
err := c.Set(foo)
|
||||||
|
checkErr(err, "first set(foo): %v", err)
|
||||||
|
err = c.Set(foo)
|
||||||
|
checkErr(err, "second set(foo): %v", err)
|
||||||
|
|
||||||
|
// Get
|
||||||
|
it, err := c.Get("foo")
|
||||||
|
checkErr(err, "get(foo): %v", err)
|
||||||
|
if it.Key != "foo" {
|
||||||
|
t.Errorf("get(foo) Key = %q, want foo", it.Key)
|
||||||
|
}
|
||||||
|
if string(it.Value) != "fooval" {
|
||||||
|
t.Errorf("get(foo) Value = %q, want fooval", string(it.Value))
|
||||||
|
}
|
||||||
|
if it.Flags != 123 {
|
||||||
|
t.Errorf("get(foo) Flags = %v, want 123", it.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and set a unicode key
|
||||||
|
quxKey := "Hello_世界"
|
||||||
|
qux := &Item{Key: quxKey, Value: []byte("hello world")}
|
||||||
|
err = c.Set(qux)
|
||||||
|
checkErr(err, "first set(Hello_世界): %v", err)
|
||||||
|
it, err = c.Get(quxKey)
|
||||||
|
checkErr(err, "get(Hello_世界): %v", err)
|
||||||
|
if it.Key != quxKey {
|
||||||
|
t.Errorf("get(Hello_世界) Key = %q, want Hello_世界", it.Key)
|
||||||
|
}
|
||||||
|
if string(it.Value) != "hello world" {
|
||||||
|
t.Errorf("get(Hello_世界) Value = %q, want hello world", string(it.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set malformed keys
|
||||||
|
malFormed := &Item{Key: "foo bar", Value: []byte("foobarval")}
|
||||||
|
err = c.Set(malFormed)
|
||||||
|
if err != ErrMalformedKey {
|
||||||
|
t.Errorf("set(foo bar) should return ErrMalformedKey instead of %v", err)
|
||||||
|
}
|
||||||
|
malFormed = &Item{Key: "foo" + string(0x7f), Value: []byte("foobarval")}
|
||||||
|
err = c.Set(malFormed)
|
||||||
|
if err != ErrMalformedKey {
|
||||||
|
t.Errorf("set(foo<0x7f>) should return ErrMalformedKey instead of %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add
|
||||||
|
bar := &Item{Key: "bar", Value: []byte("barval")}
|
||||||
|
err = c.Add(bar)
|
||||||
|
checkErr(err, "first add(foo): %v", err)
|
||||||
|
if err := c.Add(bar); err != ErrNotStored {
|
||||||
|
t.Fatalf("second add(foo) want ErrNotStored, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace
|
||||||
|
baz := &Item{Key: "baz", Value: []byte("bazvalue")}
|
||||||
|
if err := c.Replace(baz); err != ErrNotStored {
|
||||||
|
t.Fatalf("expected replace(baz) to return ErrNotStored, got %v", err)
|
||||||
|
}
|
||||||
|
err = c.Replace(bar)
|
||||||
|
checkErr(err, "replaced(foo): %v", err)
|
||||||
|
|
||||||
|
// GetMulti
|
||||||
|
m, err := c.GetMulti([]string{"foo", "bar"})
|
||||||
|
checkErr(err, "GetMulti: %v", err)
|
||||||
|
if g, e := len(m), 2; g != e {
|
||||||
|
t.Errorf("GetMulti: got len(map) = %d, want = %d", g, e)
|
||||||
|
}
|
||||||
|
if _, ok := m["foo"]; !ok {
|
||||||
|
t.Fatalf("GetMulti: didn't get key 'foo'")
|
||||||
|
}
|
||||||
|
if _, ok := m["bar"]; !ok {
|
||||||
|
t.Fatalf("GetMulti: didn't get key 'bar'")
|
||||||
|
}
|
||||||
|
if g, e := string(m["foo"].Value), "fooval"; g != e {
|
||||||
|
t.Errorf("GetMulti: foo: got %q, want %q", g, e)
|
||||||
|
}
|
||||||
|
if g, e := string(m["bar"].Value), "barval"; g != e {
|
||||||
|
t.Errorf("GetMulti: bar: got %q, want %q", g, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
err = c.Delete("foo")
|
||||||
|
checkErr(err, "Delete: %v", err)
|
||||||
|
it, err = c.Get("foo")
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Errorf("post-Delete want ErrCacheMiss, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incr/Decr
|
||||||
|
mustSet(&Item{Key: "num", Value: []byte("42")})
|
||||||
|
n, err := c.Increment("num", 8)
|
||||||
|
checkErr(err, "Increment num + 8: %v", err)
|
||||||
|
if n != 50 {
|
||||||
|
t.Fatalf("Increment num + 8: want=50, got=%d", n)
|
||||||
|
}
|
||||||
|
n, err = c.Decrement("num", 49)
|
||||||
|
checkErr(err, "Decrement: %v", err)
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("Decrement 49: want=1, got=%d", n)
|
||||||
|
}
|
||||||
|
err = c.Delete("num")
|
||||||
|
checkErr(err, "delete num: %v", err)
|
||||||
|
n, err = c.Increment("num", 1)
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Fatalf("increment post-delete: want ErrCacheMiss, got %v", err)
|
||||||
|
}
|
||||||
|
mustSet(&Item{Key: "num", Value: []byte("not-numeric")})
|
||||||
|
n, err = c.Increment("num", 1)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "client error") {
|
||||||
|
t.Fatalf("increment non-number: want client error, got %v", err)
|
||||||
|
}
|
||||||
|
testTouchWithClient(t, c)
|
||||||
|
|
||||||
|
// Test Delete All
|
||||||
|
err = c.DeleteAll()
|
||||||
|
checkErr(err, "DeleteAll: %v", err)
|
||||||
|
it, err = c.Get("bar")
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Errorf("post-DeleteAll want ErrCacheMiss, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTouchWithClient(t *testing.T, c *Client) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Log("Skipping testing memcache Touch with testing in Short mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mustSet := mustSetF(t, c)
|
||||||
|
|
||||||
|
const secondsToExpiry = int32(2)
|
||||||
|
|
||||||
|
// We will set foo and bar to expire in 2 seconds, then we'll keep touching
|
||||||
|
// foo every second
|
||||||
|
// After 3 seconds, we expect foo to be available, and bar to be expired
|
||||||
|
foo := &Item{Key: "foo", Value: []byte("fooval"), Expiration: secondsToExpiry}
|
||||||
|
bar := &Item{Key: "bar", Value: []byte("barval"), Expiration: secondsToExpiry}
|
||||||
|
|
||||||
|
setTime := time.Now()
|
||||||
|
mustSet(foo)
|
||||||
|
mustSet(bar)
|
||||||
|
|
||||||
|
for s := 0; s < 3; s++ {
|
||||||
|
time.Sleep(time.Duration(1 * time.Second))
|
||||||
|
err := c.Touch(foo.Key, secondsToExpiry)
|
||||||
|
if nil != err {
|
||||||
|
t.Errorf("error touching foo: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.Get("foo")
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrCacheMiss {
|
||||||
|
t.Fatalf("touching failed to keep item foo alive")
|
||||||
|
} else {
|
||||||
|
t.Fatalf("unexpected error retrieving foo after touching: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Get("bar")
|
||||||
|
if nil == err {
|
||||||
|
t.Fatalf("item bar did not expire within %v seconds", time.Now().Sub(setTime).Seconds())
|
||||||
|
} else {
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Fatalf("unexpected error retrieving bar: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkOnItem(b *testing.B) {
|
||||||
|
fakeServer, err := net.Listen("tcp", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal("Could not open fake server: ", err)
|
||||||
|
}
|
||||||
|
defer fakeServer.Close()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if c, err := fakeServer.Accept(); err == nil {
|
||||||
|
go func() { io.Copy(ioutil.Discard, c) }()
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
addr := fakeServer.Addr()
|
||||||
|
c := New(addr.String())
|
||||||
|
if _, err := c.getConn(addr); err != nil {
|
||||||
|
b.Fatal("failed to initialize connection to fake server")
|
||||||
|
}
|
||||||
|
|
||||||
|
item := Item{Key: "foo"}
|
||||||
|
dummyFn := func(_ *Client, _ *bufio.ReadWriter, _ *Item) error { return nil }
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c.onItem(&item, dummyFn)
|
||||||
|
}
|
||||||
|
}
|
129
vendor/github.com/bradfitz/gomemcache/memcache/selector.go
generated
vendored
Normal file
129
vendor/github.com/bradfitz/gomemcache/memcache/selector.go
generated
vendored
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2011 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash/crc32"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerSelector is the interface that selects a memcache server
|
||||||
|
// as a function of the item's key.
|
||||||
|
//
|
||||||
|
// All ServerSelector implementations must be safe for concurrent use
|
||||||
|
// by multiple goroutines.
|
||||||
|
type ServerSelector interface {
|
||||||
|
// PickServer returns the server address that a given item
|
||||||
|
// should be shared onto.
|
||||||
|
PickServer(key string) (net.Addr, error)
|
||||||
|
Each(func(net.Addr) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerList is a simple ServerSelector. Its zero value is usable.
|
||||||
|
type ServerList struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
addrs []net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticAddr caches the Network() and String() values from any net.Addr.
|
||||||
|
type staticAddr struct {
|
||||||
|
ntw, str string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStaticAddr(a net.Addr) net.Addr {
|
||||||
|
return &staticAddr{
|
||||||
|
ntw: a.Network(),
|
||||||
|
str: a.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *staticAddr) Network() string { return s.ntw }
|
||||||
|
func (s *staticAddr) String() string { return s.str }
|
||||||
|
|
||||||
|
// SetServers changes a ServerList's set of servers at runtime and is
|
||||||
|
// safe for concurrent use by multiple goroutines.
|
||||||
|
//
|
||||||
|
// Each server is given equal weight. A server is given more weight
|
||||||
|
// if it's listed multiple times.
|
||||||
|
//
|
||||||
|
// SetServers returns an error if any of the server names fail to
|
||||||
|
// resolve. No attempt is made to connect to the server. If any error
|
||||||
|
// is returned, no changes are made to the ServerList.
|
||||||
|
func (ss *ServerList) SetServers(servers ...string) error {
|
||||||
|
naddr := make([]net.Addr, len(servers))
|
||||||
|
for i, server := range servers {
|
||||||
|
if strings.Contains(server, "/") {
|
||||||
|
addr, err := net.ResolveUnixAddr("unix", server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
naddr[i] = newStaticAddr(addr)
|
||||||
|
} else {
|
||||||
|
tcpaddr, err := net.ResolveTCPAddr("tcp", server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
naddr[i] = newStaticAddr(tcpaddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.mu.Lock()
|
||||||
|
defer ss.mu.Unlock()
|
||||||
|
ss.addrs = naddr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each iterates over each server calling the given function
|
||||||
|
func (ss *ServerList) Each(f func(net.Addr) error) error {
|
||||||
|
ss.mu.RLock()
|
||||||
|
defer ss.mu.RUnlock()
|
||||||
|
for _, a := range ss.addrs {
|
||||||
|
if err := f(a); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyBufPool returns []byte buffers for use by PickServer's call to
|
||||||
|
// crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
|
||||||
|
// copies, which at least are bounded in size and small)
|
||||||
|
var keyBufPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
b := make([]byte, 256)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ServerList) PickServer(key string) (net.Addr, error) {
|
||||||
|
ss.mu.RLock()
|
||||||
|
defer ss.mu.RUnlock()
|
||||||
|
if len(ss.addrs) == 0 {
|
||||||
|
return nil, ErrNoServers
|
||||||
|
}
|
||||||
|
if len(ss.addrs) == 1 {
|
||||||
|
return ss.addrs[0], nil
|
||||||
|
}
|
||||||
|
bufp := keyBufPool.Get().(*[]byte)
|
||||||
|
n := copy(*bufp, key)
|
||||||
|
cs := crc32.ChecksumIEEE((*bufp)[:n])
|
||||||
|
keyBufPool.Put(bufp)
|
||||||
|
|
||||||
|
return ss.addrs[cs%uint32(len(ss.addrs))], nil
|
||||||
|
}
|
39
vendor/github.com/bradfitz/gomemcache/memcache/selector_test.go
generated
vendored
Normal file
39
vendor/github.com/bradfitz/gomemcache/memcache/selector_test.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func BenchmarkPickServer(b *testing.B) {
|
||||||
|
// at least two to avoid 0 and 1 special cases:
|
||||||
|
benchPickServer(b, "127.0.0.1:1234", "127.0.0.1:1235")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPickServer_Single(b *testing.B) {
|
||||||
|
benchPickServer(b, "127.0.0.1:1234")
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchPickServer(b *testing.B, servers ...string) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
var ss ServerList
|
||||||
|
ss.SetServers(servers...)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := ss.PickServer("some key"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/.gitignore
generated
vendored
Normal file
23
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/.gitignore
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
201
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/LICENSE
generated
vendored
Normal file
201
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/LICENSE
generated
vendored
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
103
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/README.md
generated
vendored
Normal file
103
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/README.md
generated
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
gorilla-sessions-memcache
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Memcache session support for Gorilla Web Toolkit.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
The usual gorilla stuff:
|
||||||
|
|
||||||
|
go get github.com/gorilla/sessions
|
||||||
|
|
||||||
|
Plus Brad Fitz' memcache client:
|
||||||
|
|
||||||
|
go get github.com/bradfitz/gomemcache/memcache
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
gsm "github.com/bradleypeabody/gorilla-sessions-memcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
// set up your memcache client
|
||||||
|
memcacheClient := memcache.New("localhost:11211")
|
||||||
|
|
||||||
|
// set up your session store
|
||||||
|
store := gsm.NewMemcacheStore(memcacheClient, "session_prefix_", []byte("secret-key-goes-here"))
|
||||||
|
|
||||||
|
// and the rest of it is the same as any other gorilla session handling:
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, _ := store.Get(r, "session-name")
|
||||||
|
session.Values["foo"] = "bar"
|
||||||
|
session.Values[42] = 43
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
......
|
||||||
|
// you can also setup a MemCacheStore, which does not rely on the browser accepting cookies.
|
||||||
|
// this means, your client has to extract and send a configurable http Headerfield manually.
|
||||||
|
// e.g.
|
||||||
|
|
||||||
|
// set up your memcache client
|
||||||
|
memcacheClient := memcache.New("localhost:11211")
|
||||||
|
|
||||||
|
// set up your session store relying on a http Headerfield: `X-CUSTOM-HEADER`
|
||||||
|
store := gsm.NewMemcacheStoreWithValueStorer(memcacheClient, &gsm.HeaderStorer{HeaderPrefix:"X-CUSTOM-HEADER"}, "session_prefix_", []byte("secret-key-goes-here"))
|
||||||
|
|
||||||
|
// and the rest of it is the same as any other gorilla session handling:
|
||||||
|
// The client has to send the session information in the header-field: `X-CUSTOM-HEADER`
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, _ := store.Get(r, "session-name")
|
||||||
|
session.Values["foo"] = "bar"
|
||||||
|
session.Values[42] = 43
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage Methods
|
||||||
|
---------------
|
||||||
|
|
||||||
|
I've added a few different methods of storage of the session data in memcache. You
|
||||||
|
use them by setting the StoreMethod field.
|
||||||
|
|
||||||
|
* SecureCookie - uses the default securecookie encoding. Values are more secure
|
||||||
|
as they are not readable from memcache without the secret key.
|
||||||
|
* Gob - uses the Gob encoder directly without any post processing. Faster.
|
||||||
|
Result is Gob's usual binary gibber (not human readable)
|
||||||
|
* Json - uses the Json Marshaller. Result is human readable, slower but still
|
||||||
|
pretty fast. Be careful - it will munch your data into stuff that works
|
||||||
|
with JSON, and the keys must be strings. Example: you put in an int64 value
|
||||||
|
and you'll get back a float64.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
store := gsm.NewMemcacheStore(memcacheClient, "session_prefix_", []byte("..."))
|
||||||
|
// do one of these:
|
||||||
|
store.StoreMethod = gsm.StoreMethodSecureCookie // default, more secure
|
||||||
|
store.StoreMethod = gsm.StoreMethodGob // faster
|
||||||
|
store.StoreMethod = gsm.StoreMethodJson // human readable
|
||||||
|
// (but watch out, it munches your types
|
||||||
|
// to JSON compatible stuff)
|
||||||
|
|
||||||
|
Logging
|
||||||
|
-------
|
||||||
|
|
||||||
|
Logging is available by setting the Logging field to > 0 after making your MemcacheStore.
|
||||||
|
|
||||||
|
store := gsm.NewMemcacheStore(memcacheClient, "session_prefix_", []byte("..."))
|
||||||
|
store.Logging = 1
|
||||||
|
|
||||||
|
That will output (using log.Printf) data about each session read/written from/to memcache.
|
||||||
|
Useful for debugging
|
||||||
|
|
||||||
|
Things to Know
|
||||||
|
--------------
|
||||||
|
|
||||||
|
* This is still experimental as of May 2014.
|
||||||
|
|
||||||
|
* You can also call NewDumbMemorySessionStore() for local development without a memcache server (it's a stub that just stuffs your session data in a map - definitely do not use this for anything but local dev and testing).
|
295
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsm.go
generated
vendored
Normal file
295
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsm.go
generated
vendored
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
// Memcache session support for Gorilla Web Toolkit,
|
||||||
|
// without Google App Engine dependency.
|
||||||
|
package gsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMemcacheStoreWithValueStorer returns a new MemcacheStore backed by a ValueStorer.
|
||||||
|
// You need to provide the memcache client and
|
||||||
|
// an optional prefix for the keys we store.
|
||||||
|
// A ValueStorer is used to store an encrypted sessionID. The encrypted sessionID is used to access
|
||||||
|
// memcache and get the session values.
|
||||||
|
func NewMemcacheStoreWithValueStorer(client *memcache.Client, valueStorer ValueStorer, keyPrefix string, keyPairs ...[]byte) *MemcacheStore {
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
panic("Cannot have nil memcache client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if valueStorer == nil {
|
||||||
|
panic("Cannot have nil ValueStorer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MemcacheStore{
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||||
|
Options: &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 30,
|
||||||
|
},
|
||||||
|
KeyPrefix: keyPrefix,
|
||||||
|
Client: client,
|
||||||
|
StoreMethod: StoreMethodSecureCookie,
|
||||||
|
ValueStorer: valueStorer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemcacheStore returns a new MemcacheStore.
|
||||||
|
// You need to provide the memcache client and
|
||||||
|
// an optional prefix for the keys we store
|
||||||
|
func NewMemcacheStore(client *memcache.Client, keyPrefix string, keyPairs ...[]byte) *MemcacheStore {
|
||||||
|
return NewMemcacheStoreWithValueStorer(client, &CookieStorer{}, keyPrefix, keyPairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreMethod string
|
||||||
|
|
||||||
|
// take your pick on how to store the values in memcache
|
||||||
|
const (
|
||||||
|
StoreMethodSecureCookie = StoreMethod("securecookie") // security
|
||||||
|
StoreMethodGob = StoreMethod("gob") // speed
|
||||||
|
StoreMethodJson = StoreMethod("json") // simplicity; warning: only string keys allowed and rest of data must be JSON.Marshal compatible
|
||||||
|
)
|
||||||
|
|
||||||
|
// MemcacheStore stores sessions in memcache
|
||||||
|
//
|
||||||
|
type MemcacheStore struct {
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *sessions.Options // default configuration
|
||||||
|
Client *memcache.Client
|
||||||
|
KeyPrefix string
|
||||||
|
Logging int // set to > 0 to enable logging (using log.Printf)
|
||||||
|
StoreMethod StoreMethod
|
||||||
|
ValueStorer ValueStorer
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxLength restricts the maximum length of new sessions to l.
|
||||||
|
// If l is 0 there is no limit to the size of a session, use with caution.
|
||||||
|
// The default for a new MemcacheStore is 4096.
|
||||||
|
func (s *MemcacheStore) MaxLength(l int) {
|
||||||
|
for _, c := range s.Codecs {
|
||||||
|
if codec, ok := c.(*securecookie.SecureCookie); ok {
|
||||||
|
codec.MaxLength(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.Get().
|
||||||
|
func (s *MemcacheStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
return sessions.GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.New().
|
||||||
|
func (s *MemcacheStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
session := sessions.NewSession(s, name)
|
||||||
|
opts := *s.Options
|
||||||
|
session.Options = &opts
|
||||||
|
session.IsNew = true
|
||||||
|
var err error
|
||||||
|
if value, errCookie := s.ValueStorer.GetValueForSessionName(r, name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, value, &session.ID, s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
err = s.load(session)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *MemcacheStore) Save(r *http.Request, w http.ResponseWriter,
|
||||||
|
session *sessions.Session) error {
|
||||||
|
if session.ID == "" {
|
||||||
|
// Because the ID is used in the filename, encode it to
|
||||||
|
// use alphanumeric characters only.
|
||||||
|
session.ID = strings.TrimRight(
|
||||||
|
base32.StdEncoding.EncodeToString(
|
||||||
|
securecookie.GenerateRandomKey(32)), "=")
|
||||||
|
}
|
||||||
|
if err := s.save(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.ValueStorer.SetValueForSessionName(w, session.Name(), encoded, session.Options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save writes encoded session.Values using the memcache client
|
||||||
|
func (s *MemcacheStore) save(session *sessions.Session) error {
|
||||||
|
|
||||||
|
key := s.KeyPrefix + session.ID
|
||||||
|
|
||||||
|
switch s.StoreMethod {
|
||||||
|
|
||||||
|
case StoreMethodSecureCookie:
|
||||||
|
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: securecookie, encoding error: %v)", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Client.Set(&memcache.Item{Key: key, Value: []byte(encoded), Expiration: int32(session.Options.MaxAge)})
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: securecookie, session name: %v, memcache key: %v, memcache value: %v, error: %v)", session.Name(), key, encoded, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case StoreMethodGob:
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
enc := gob.NewEncoder(buf)
|
||||||
|
if err := enc.Encode(session.Values); err != nil {
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: gob, encoding error: %v)", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bufbytes := buf.Bytes()
|
||||||
|
|
||||||
|
err := s.Client.Set(&memcache.Item{Key: key, Value: bufbytes, Expiration: int32(session.Options.MaxAge)})
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: gob, session name: %v, memcache key: %v, memcache value len: %v, error: %v)", session.Name(), key, len(bufbytes), err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case StoreMethodJson:
|
||||||
|
|
||||||
|
vals := make(map[string]interface{}, len(session.Values))
|
||||||
|
for k, v := range session.Values {
|
||||||
|
ks, ok := k.(string)
|
||||||
|
if !ok {
|
||||||
|
err := fmt.Errorf("Non-string key value, cannot jsonize: %v", k)
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: json, encoding error: %v)", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vals[ks] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
bufbytes, err := json.Marshal(vals)
|
||||||
|
if err != nil {
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: json, encoding error: %v)", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Client.Set(&memcache.Item{Key: key, Value: bufbytes, Expiration: int32(session.Options.MaxAge)})
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: set (method: json, session name: %v, memcache key: %v, memcache value: %v, error: %v)", session.Name(), key, string(bufbytes), err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("Unknown StoreMethod: " + string(s.StoreMethod))
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("Unreachable")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load reads a file and decodes its content into session.Values.
|
||||||
|
func (s *MemcacheStore) load(session *sessions.Session) error {
|
||||||
|
|
||||||
|
key := s.KeyPrefix + session.ID
|
||||||
|
|
||||||
|
it, err := s.Client.Get(key)
|
||||||
|
if s.Logging > 0 {
|
||||||
|
if s.StoreMethod == StoreMethodJson {
|
||||||
|
log.Printf("gorilla-sessions-memcache: get (method: %s, session name: %v, memcache key: %v, memcache value: %v, error: %v)", s.StoreMethod, session.Name(), key, string(it.Value), err)
|
||||||
|
} else {
|
||||||
|
log.Printf("gorilla-sessions-memcache: get (method: %s, session name: %v, memcache key: %v, memcache value len: %v, error: %v)", s.StoreMethod, session.Name(), key, len(it.Value), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.StoreMethod {
|
||||||
|
|
||||||
|
case StoreMethodSecureCookie:
|
||||||
|
|
||||||
|
if err = securecookie.DecodeMulti(session.Name(), string(it.Value),
|
||||||
|
&session.Values, s.Codecs...); err != nil {
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: get (method: securecookie, decoding error: %v)", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case StoreMethodGob:
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(it.Value)
|
||||||
|
dec := gob.NewDecoder(buf)
|
||||||
|
|
||||||
|
err = dec.Decode(&session.Values)
|
||||||
|
if err != nil {
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: get (method: gob, decoding error: %v)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
|
||||||
|
case StoreMethodJson:
|
||||||
|
|
||||||
|
vals := make(map[string]interface{})
|
||||||
|
|
||||||
|
err := json.Unmarshal(it.Value, &vals)
|
||||||
|
if err != nil {
|
||||||
|
if s.Logging > 0 {
|
||||||
|
log.Printf("gorilla-sessions-memcache: get (method: json, decoding error: %v)", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range vals {
|
||||||
|
session.Values[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("Unknown StoreMethod: " + string(s.StoreMethod))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("Unreachable")
|
||||||
|
return nil
|
||||||
|
}
|
174
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsm_test.go
generated
vendored
Normal file
174
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsm_test.go
generated
vendored
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package gsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME: need unit tests for the different StoreMethods
|
||||||
|
|
||||||
|
func TestMain(t *testing.T) {
|
||||||
|
|
||||||
|
doRun := func(method StoreMethod, listenConfig string) {
|
||||||
|
|
||||||
|
memcacheClient := memcache.New("localhost:11211")
|
||||||
|
// fmt.Printf("memcacheClient = %v\n", memcacheClient)
|
||||||
|
sessionStore := NewMemcacheStore(memcacheClient, "TestMain_", []byte("example123"))
|
||||||
|
sessionStore.StoreMethod = StoreMethod(method)
|
||||||
|
sessionStore.Logging = 1
|
||||||
|
|
||||||
|
http.HandleFunc("/test"+string(method), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
|
||||||
|
|
||||||
|
session, _ := sessionStore.Get(r, "example")
|
||||||
|
|
||||||
|
storeval := r.FormValue("store")
|
||||||
|
if len(storeval) > 0 {
|
||||||
|
session.Values["thevalue"] = storeval
|
||||||
|
} else {
|
||||||
|
storeval, _ = session.Values["thevalue"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error while saving session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s", storeval)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// run the server
|
||||||
|
go http.ListenAndServe(listenConfig, nil)
|
||||||
|
|
||||||
|
// now do some tests as a client make sure things work as expected
|
||||||
|
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
|
||||||
|
doReq := func(u string) string {
|
||||||
|
resp, err := httpClient.Get(u)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Got Set-Cookie: %s\n", resp.Header.Get("Set-Cookie"))
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := doReq("http://localhost" + listenConfig + "/test" + string(method) + "?store=blah")
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
v = doReq("http://localhost" + listenConfig + "/test" + string(method))
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected session to give us v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
doRun(StoreMethodSecureCookie, ":18201")
|
||||||
|
doRun(StoreMethodGob, ":18202")
|
||||||
|
doRun(StoreMethodJson, ":18203")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMainHeaderStorer tests storing the secure sessionID in a configurable http HEADER field
|
||||||
|
// and then fetching the session information via Memcache.
|
||||||
|
func TestMainHeaderStorer(t *testing.T) {
|
||||||
|
headerName := "X-TEST-HEADER"
|
||||||
|
|
||||||
|
doRun := func(method StoreMethod, listenConfig string) {
|
||||||
|
|
||||||
|
headerStorer := &HeaderStorer{HeaderFieldName: headerName}
|
||||||
|
memcacheClient := memcache.New("localhost:11211")
|
||||||
|
// fmt.Printf("memcacheClient = %v\n", memcacheClient)
|
||||||
|
sessionStore := NewMemcacheStoreWithValueStorer(memcacheClient, headerStorer, "TestMain_", []byte("example123"))
|
||||||
|
sessionStore.StoreMethod = StoreMethod(method)
|
||||||
|
sessionStore.Logging = 1
|
||||||
|
|
||||||
|
http.HandleFunc("/testHeaderStorer"+string(method), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
|
||||||
|
|
||||||
|
session, _ := sessionStore.Get(r, "example")
|
||||||
|
|
||||||
|
storeval := r.FormValue("store")
|
||||||
|
if len(storeval) > 0 {
|
||||||
|
session.Values["thevalue"] = storeval
|
||||||
|
} else {
|
||||||
|
storeval, _ = session.Values["thevalue"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error while saving session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s", storeval)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// run the server
|
||||||
|
go http.ListenAndServe(listenConfig, nil)
|
||||||
|
|
||||||
|
// now do some tests as a client make sure things work as expected
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
|
||||||
|
doReq := func(req *http.Request) (string, string, string) {
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
value := resp.Header.Get(headerName)
|
||||||
|
fmt.Printf("Got %s: %s\n", headerName, value)
|
||||||
|
|
||||||
|
return string(b), headerName, value
|
||||||
|
}
|
||||||
|
|
||||||
|
// the first request should return a headerKey and headerValue, which contain a securely encrypted sessionID.
|
||||||
|
// subsequent requests should sent this sessionID along so the server can fetch the session from the memcache store.
|
||||||
|
req, _ := http.NewRequest("GET", "http://localhost"+listenConfig+"/testHeaderStorer"+string(method)+"?store=blah", nil)
|
||||||
|
v, headerKey, headerValue := doReq(req)
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost"+listenConfig+"/testHeaderStorer"+string(method), nil)
|
||||||
|
// sent header along with secure sessionID
|
||||||
|
req.Header.Add(headerKey, headerValue)
|
||||||
|
v, _, _ = doReq(req)
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected session to give us v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
doRun(StoreMethodSecureCookie, ":18201")
|
||||||
|
doRun(StoreMethodGob, ":18202")
|
||||||
|
doRun(StoreMethodJson, ":18203")
|
||||||
|
|
||||||
|
}
|
139
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsmstub.go
generated
vendored
Normal file
139
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsmstub.go
generated
vendored
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package gsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base32"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDumbMemorySessionStoreWithValueStorer return a new dumb in-memory
|
||||||
|
// map and no expiration backed by a ValueStorer. Good for local
|
||||||
|
// development so you don't have to run
|
||||||
|
// memcached on your laptop just to fire up
|
||||||
|
// your app and hack away.
|
||||||
|
// A ValueStorer is used to store an encrypted sessionID. The encrypted sessionID is used to access
|
||||||
|
// the dumb in-memory map and get the session values.
|
||||||
|
func NewDumbMemorySessionStoreWithValueStorer(valueStorer ValueStorer) *DumbMemoryStore {
|
||||||
|
|
||||||
|
if valueStorer == nil {
|
||||||
|
panic("Cannot have nil ValueStorer")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPair := []byte("stub")
|
||||||
|
|
||||||
|
return &DumbMemoryStore{
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPair),
|
||||||
|
Options: &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 30,
|
||||||
|
},
|
||||||
|
Data: make(map[string]string),
|
||||||
|
ValueStorer: valueStorer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sessions implemented with a dumb in-memory
|
||||||
|
// map and no expiration. Good for local
|
||||||
|
// development so you don't have to run
|
||||||
|
// memcached on your laptop just to fire up
|
||||||
|
// your app and hack away.
|
||||||
|
func NewDumbMemorySessionStore() *DumbMemoryStore {
|
||||||
|
return NewDumbMemorySessionStoreWithValueStorer(&CookieStorer{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumbMemoryStore stores sessions in memcache
|
||||||
|
//
|
||||||
|
type DumbMemoryStore struct {
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *sessions.Options // default configuration
|
||||||
|
Data map[string]string // session data goes here
|
||||||
|
ValueStorer ValueStorer
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxLength restricts the maximum length of new sessions to l.
|
||||||
|
// If l is 0 there is no limit to the size of a session, use with caution.
|
||||||
|
// The default for a new DumbMemoryStore is 4096.
|
||||||
|
func (s *DumbMemoryStore) MaxLength(l int) {
|
||||||
|
for _, c := range s.Codecs {
|
||||||
|
if codec, ok := c.(*securecookie.SecureCookie); ok {
|
||||||
|
codec.MaxLength(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.Get().
|
||||||
|
func (s *DumbMemoryStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
return sessions.GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.New().
|
||||||
|
func (s *DumbMemoryStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
session := sessions.NewSession(s, name)
|
||||||
|
opts := *s.Options
|
||||||
|
session.Options = &opts
|
||||||
|
session.IsNew = true
|
||||||
|
var err error
|
||||||
|
if value, errCookie := s.ValueStorer.GetValueForSessionName(r, name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, value, &session.ID, s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
err = s.load(session)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *DumbMemoryStore) Save(r *http.Request, w http.ResponseWriter,
|
||||||
|
session *sessions.Session) error {
|
||||||
|
if session.ID == "" {
|
||||||
|
// Because the ID is used in the filename, encode it to
|
||||||
|
// use alphanumeric characters only.
|
||||||
|
session.ID = strings.TrimRight(
|
||||||
|
base32.StdEncoding.EncodeToString(
|
||||||
|
securecookie.GenerateRandomKey(32)), "=")
|
||||||
|
}
|
||||||
|
if err := s.save(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.ValueStorer.SetValueForSessionName(w, session.Name(), encoded, session.Options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save writes encoded session.Values using the memcache client
|
||||||
|
func (s *DumbMemoryStore) save(session *sessions.Session) error {
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Data[session.ID] = encoded
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load reads a file and decodes its content into session.Values.
|
||||||
|
func (s *DumbMemoryStore) load(session *sessions.Session) error {
|
||||||
|
|
||||||
|
if err := securecookie.DecodeMulti(session.Name(), string(s.Data[session.ID]),
|
||||||
|
&session.Values, s.Codecs...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
145
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsmstub_test.go
generated
vendored
Normal file
145
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/gsmstub_test.go
generated
vendored
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package gsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStubMain(t *testing.T) {
|
||||||
|
|
||||||
|
sessionStore := NewDumbMemorySessionStore()
|
||||||
|
|
||||||
|
http.HandleFunc("/testdumb", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
|
||||||
|
|
||||||
|
session, _ := sessionStore.Get(r, "example")
|
||||||
|
|
||||||
|
storeval := r.FormValue("store")
|
||||||
|
if len(storeval) > 0 {
|
||||||
|
session.Values["thevalue"] = storeval
|
||||||
|
} else {
|
||||||
|
storeval, _ = session.Values["thevalue"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error while saving session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s", storeval)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// run the server
|
||||||
|
go http.ListenAndServe(":18210", nil)
|
||||||
|
|
||||||
|
// now do some tests as a client make sure things work as expected
|
||||||
|
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
|
||||||
|
doReq := func(u string) string {
|
||||||
|
resp, err := httpClient.Get(u)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Got Set-Cookie: %s\n", resp.Header.Get("Set-Cookie"))
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := doReq("http://localhost:18210/testdumb?store=blah")
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
v = doReq("http://localhost:18210/testdumb")
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected session to give us v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStubHeaderStorer(t *testing.T) {
|
||||||
|
headerName := "X-TEST-HEADER"
|
||||||
|
headerStorer := &HeaderStorer{HeaderFieldName: headerName}
|
||||||
|
|
||||||
|
sessionStore := NewDumbMemorySessionStoreWithValueStorer(headerStorer)
|
||||||
|
|
||||||
|
http.HandleFunc("/testdumbHeaderStorer", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
|
||||||
|
|
||||||
|
session, _ := sessionStore.Get(r, "example")
|
||||||
|
|
||||||
|
storeval := r.FormValue("store")
|
||||||
|
if len(storeval) > 0 {
|
||||||
|
session.Values["thevalue"] = storeval
|
||||||
|
} else {
|
||||||
|
storeval, _ = session.Values["thevalue"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error while saving session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s", storeval)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// run the server
|
||||||
|
go http.ListenAndServe(":18210", nil)
|
||||||
|
|
||||||
|
// now do some tests as a client make sure things work as expected
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
|
||||||
|
doReq := func(req *http.Request) (string, string, string) {
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
value := resp.Header.Get(headerName)
|
||||||
|
fmt.Printf("Got %s: %s\n", headerName, value)
|
||||||
|
|
||||||
|
return string(b), headerName, value
|
||||||
|
}
|
||||||
|
|
||||||
|
// the first request should return a headerKey and headerValue, which contain a securely encrypted sessionID.
|
||||||
|
// subsequent requests should sent this sessionID along so the server can fetch the session from the memcache store.
|
||||||
|
req, _ := http.NewRequest("GET", "http://localhost:18210/testdumbHeaderStorer?store=blah", nil)
|
||||||
|
v, headerKey, headerValue := doReq(req)
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ = http.NewRequest("GET", "http://localhost:18210/testdumbHeaderStorer", nil)
|
||||||
|
// sent header along with secure sessionID
|
||||||
|
req.Header.Add(headerKey, headerValue)
|
||||||
|
v, _, _ = doReq(req)
|
||||||
|
if v != "blah" {
|
||||||
|
t.Fatalf("Expected session to give us v=blah but got v='%s'\n", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
127
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/valuestorer.go
generated
vendored
Normal file
127
vendor/github.com/bradleypeabody/gorilla-sessions-memcache/valuestorer.go
generated
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package gsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrHeaderFieldNameEmpty is returned, if the HeaderFieldName, which should be used to store session information, is empty.
|
||||||
|
ErrHeaderFieldNameEmpty = errors.New("header fieldname empty")
|
||||||
|
|
||||||
|
// ErrValueNotFound is returned, if no value was found for a given sessionName.
|
||||||
|
ErrValueNotFound = errors.New("value not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValueStorer stores a value for a given name inside a http.Request.
|
||||||
|
// The value is typically the encrypted sessionID, which can then be
|
||||||
|
// fetched by a Gorialla sessions.Store implementation.
|
||||||
|
type ValueStorer interface {
|
||||||
|
// GetValueForSessionName gets a value string using it's underlying ValueStorer implementation.
|
||||||
|
GetValueForSessionName(r *http.Request, name string) (string, error)
|
||||||
|
|
||||||
|
// SetValueForSessionName sets a value string using it's underlying ValueStorer implementation.
|
||||||
|
SetValueForSessionName(w http.ResponseWriter, name, value string, options *sessions.Options) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieStorer is a ValueStorer, which stores values inside an http.Cookie
|
||||||
|
type CookieStorer struct{}
|
||||||
|
|
||||||
|
// GetValueForSessionName gets a value string from an http.Cookie, which should be present in the http.Request.
|
||||||
|
func (s *CookieStorer) GetValueForSessionName(r *http.Request, name string) (string, error) {
|
||||||
|
c, err := r.Cookie(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return c.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValueForSessionName sets a value string by creating a new http.Cookie and setting a `Set-Cookie` header
|
||||||
|
func (s *CookieStorer) SetValueForSessionName(w http.ResponseWriter, name, value string, options *sessions.Options) error {
|
||||||
|
http.SetCookie(w, sessions.NewCookie(name, value, options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderStorer is a ValueStorer, which stores values inside an http Header.
|
||||||
|
// The key of the header contains can be configured using the `HeaderFieldName` variable.
|
||||||
|
// The header value is a Base64 encoded JSON map, whereas the keys of the map are the sessionName.
|
||||||
|
type HeaderStorer struct {
|
||||||
|
HeaderFieldName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueForSessionName gets a value string from an http.Header.
|
||||||
|
func (s *HeaderStorer) GetValueForSessionName(r *http.Request, name string) (string, error) {
|
||||||
|
// fetch header field from header.
|
||||||
|
headerBase64Encoded := r.Header.Get(s.HeaderFieldName)
|
||||||
|
if headerBase64Encoded == "" {
|
||||||
|
return "", ErrValueNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch value for name from JSON map.
|
||||||
|
headerMap, err := s.headerToMap(headerBase64Encoded)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
value, exists := headerMap[name]
|
||||||
|
if !exists {
|
||||||
|
return "", ErrValueNotFound
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValueForSessionName sets a value string by creating a new http.Header using the header key given by the headerStorer.HeaderKey function.
|
||||||
|
func (s *HeaderStorer) SetValueForSessionName(w http.ResponseWriter, name, value string, options *sessions.Options) error {
|
||||||
|
var newHeaderMap map[string]string
|
||||||
|
|
||||||
|
// try to fetch an existing headerMap to we can append our values
|
||||||
|
headerBase64Encoded := w.Header().Get(s.HeaderFieldName)
|
||||||
|
if headerBase64Encoded != "" {
|
||||||
|
currentHeaderMap, err := s.headerToMap(headerBase64Encoded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// we found old values. Prepare newHeaderMap, so we can add/update values.
|
||||||
|
newHeaderMap = currentHeaderMap
|
||||||
|
} else {
|
||||||
|
// no header found. add a new one.
|
||||||
|
newHeaderMap = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add/update value to map.
|
||||||
|
newHeaderMap[name] = value
|
||||||
|
|
||||||
|
// encode to base64 string
|
||||||
|
newHeaderEncoded, err := s.mapToHeader(newHeaderMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// add/replace current header
|
||||||
|
w.Header().Set(s.HeaderFieldName, newHeaderEncoded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// headerToMap decodes a base64 encoded JSON map into a regular JSON map.
|
||||||
|
func (s *HeaderStorer) headerToMap(headerBase64Encoded string) (map[string]string, error) {
|
||||||
|
headerJson, err := base64.StdEncoding.DecodeString(headerBase64Encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(headerJson), &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapToHeader encoded a JSON map into a base64 encoded string.
|
||||||
|
func (s *HeaderStorer) mapToHeader(headerMap map[string]string) (string, error) {
|
||||||
|
result, err := json.Marshal(headerMap)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(result), nil
|
||||||
|
}
|
5
vendor/github.com/garyburd/redigo/.github/CONTRIBUTING.md
generated
vendored
Normal file
5
vendor/github.com/garyburd/redigo/.github/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Ask questions at
|
||||||
|
[StackOverflow](https://stackoverflow.com/questions/ask?tags=go+redis).
|
||||||
|
|
||||||
|
[Open an issue](https://github.com/garyburd/redigo/issues/new) to discuss your
|
||||||
|
plans before doing any work on Redigo.
|
1
vendor/github.com/garyburd/redigo/.github/ISSUE_TEMPLATE.md
generated
vendored
Normal file
1
vendor/github.com/garyburd/redigo/.github/ISSUE_TEMPLATE.md
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis
|
18
vendor/github.com/garyburd/redigo/.travis.yml
generated
vendored
Normal file
18
vendor/github.com/garyburd/redigo/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
services:
|
||||||
|
- redis-server
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
- 1.6
|
||||||
|
- 1.7
|
||||||
|
- 1.8
|
||||||
|
- tip
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go get -t -v ./...
|
||||||
|
- diff -u <(echo -n) <(gofmt -d .)
|
||||||
|
- go vet $(go list ./... | grep -v /vendor/)
|
||||||
|
- go test -v -race ./...
|
175
vendor/github.com/garyburd/redigo/LICENSE
generated
vendored
Normal file
175
vendor/github.com/garyburd/redigo/LICENSE
generated
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
50
vendor/github.com/garyburd/redigo/README.markdown
generated
vendored
Normal file
50
vendor/github.com/garyburd/redigo/README.markdown
generated
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
Redigo
|
||||||
|
======
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/garyburd/redigo.svg?branch=master)](https://travis-ci.org/garyburd/redigo)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/garyburd/redigo/redis?status.svg)](https://godoc.org/github.com/garyburd/redigo/redis)
|
||||||
|
|
||||||
|
Redigo is a [Go](http://golang.org/) client for the [Redis](http://redis.io/) database.
|
||||||
|
|
||||||
|
Features
|
||||||
|
-------
|
||||||
|
|
||||||
|
* A [Print-like](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Executing_Commands) API with support for all Redis commands.
|
||||||
|
* [Pipelining](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Pipelining), including pipelined transactions.
|
||||||
|
* [Publish/Subscribe](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Publish_and_Subscribe).
|
||||||
|
* [Connection pooling](http://godoc.org/github.com/garyburd/redigo/redis#Pool).
|
||||||
|
* [Script helper type](http://godoc.org/github.com/garyburd/redigo/redis#Script) with optimistic use of EVALSHA.
|
||||||
|
* [Helper functions](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Reply_Helpers) for working with command replies.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- [API Reference](http://godoc.org/github.com/garyburd/redigo/redis)
|
||||||
|
- [FAQ](https://github.com/garyburd/redigo/wiki/FAQ)
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Install Redigo using the "go get" command:
|
||||||
|
|
||||||
|
go get github.com/garyburd/redigo/redis
|
||||||
|
|
||||||
|
The Go distribution is Redigo's only dependency.
|
||||||
|
|
||||||
|
Related Projects
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- [rafaeljusto/redigomock](https://godoc.org/github.com/rafaeljusto/redigomock) - A mock library for Redigo.
|
||||||
|
- [chasex/redis-go-cluster](https://github.com/chasex/redis-go-cluster) - A Redis cluster client implementation.
|
||||||
|
- [FZambia/go-sentinel](https://github.com/FZambia/go-sentinel) - Redis Sentinel support for Redigo
|
||||||
|
- [PuerkitoBio/redisc](https://github.com/PuerkitoBio/redisc) - Redis Cluster client built on top of Redigo
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](https://github.com/garyburd/redigo/blob/master/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Redigo is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
|
54
vendor/github.com/garyburd/redigo/internal/commandinfo.go
generated
vendored
Normal file
54
vendor/github.com/garyburd/redigo/internal/commandinfo.go
generated
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2014 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package internal // import "github.com/garyburd/redigo/internal"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WatchState = 1 << iota
|
||||||
|
MultiState
|
||||||
|
SubscribeState
|
||||||
|
MonitorState
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandInfo struct {
|
||||||
|
Set, Clear int
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandInfos = map[string]CommandInfo{
|
||||||
|
"WATCH": {Set: WatchState},
|
||||||
|
"UNWATCH": {Clear: WatchState},
|
||||||
|
"MULTI": {Set: MultiState},
|
||||||
|
"EXEC": {Clear: WatchState | MultiState},
|
||||||
|
"DISCARD": {Clear: WatchState | MultiState},
|
||||||
|
"PSUBSCRIBE": {Set: SubscribeState},
|
||||||
|
"SUBSCRIBE": {Set: SubscribeState},
|
||||||
|
"MONITOR": {Set: MonitorState},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for n, ci := range commandInfos {
|
||||||
|
commandInfos[strings.ToLower(n)] = ci
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LookupCommandInfo(commandName string) CommandInfo {
|
||||||
|
if ci, ok := commandInfos[commandName]; ok {
|
||||||
|
return ci
|
||||||
|
}
|
||||||
|
return commandInfos[strings.ToUpper(commandName)]
|
||||||
|
}
|
27
vendor/github.com/garyburd/redigo/internal/commandinfo_test.go
generated
vendored
Normal file
27
vendor/github.com/garyburd/redigo/internal/commandinfo_test.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLookupCommandInfo(t *testing.T) {
|
||||||
|
for _, n := range []string{"watch", "WATCH", "wAtch"} {
|
||||||
|
if LookupCommandInfo(n) == (CommandInfo{}) {
|
||||||
|
t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkLookupCommandInfo(b *testing.B, names ...string) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, c := range names {
|
||||||
|
LookupCommandInfo(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLookupCommandInfoCorrectCase(b *testing.B) {
|
||||||
|
benchmarkLookupCommandInfo(b, "watch", "WATCH", "monitor", "MONITOR")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLookupCommandInfoMixedCase(b *testing.B) {
|
||||||
|
benchmarkLookupCommandInfo(b, "wAtch", "WeTCH", "monItor", "MONiTOR")
|
||||||
|
}
|
68
vendor/github.com/garyburd/redigo/internal/redistest/testdb.go
generated
vendored
Normal file
68
vendor/github.com/garyburd/redigo/internal/redistest/testdb.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2014 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
// Package redistest contains utilities for writing Redigo tests.
|
||||||
|
package redistest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testConn struct {
|
||||||
|
redis.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t testConn) Close() error {
|
||||||
|
_, err := t.Conn.Do("SELECT", "9")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err = t.Conn.Do("FLUSHDB")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial dials the local Redis server and selects database 9. To prevent
|
||||||
|
// stomping on real data, DialTestDB fails if database 9 contains data. The
|
||||||
|
// returned connection flushes database 9 on close.
|
||||||
|
func Dial() (redis.Conn, error) {
|
||||||
|
c, err := redis.DialTimeout("tcp", ":6379", 0, 1*time.Second, 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Do("SELECT", "9")
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := redis.Int(c.Do("DBSIZE"))
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 0 {
|
||||||
|
c.Close()
|
||||||
|
return nil, errors.New("database #9 is not empty, test can not continue")
|
||||||
|
}
|
||||||
|
|
||||||
|
return testConn{c}, nil
|
||||||
|
}
|
618
vendor/github.com/garyburd/redigo/redis/conn.go
generated
vendored
Normal file
618
vendor/github.com/garyburd/redigo/redis/conn.go
generated
vendored
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conn is the low-level implementation of Conn
|
||||||
|
type conn struct {
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
mu sync.Mutex
|
||||||
|
pending int
|
||||||
|
err error
|
||||||
|
conn net.Conn
|
||||||
|
|
||||||
|
// Read
|
||||||
|
readTimeout time.Duration
|
||||||
|
br *bufio.Reader
|
||||||
|
|
||||||
|
// Write
|
||||||
|
writeTimeout time.Duration
|
||||||
|
bw *bufio.Writer
|
||||||
|
|
||||||
|
// Scratch space for formatting argument length.
|
||||||
|
// '*' or '$', length, "\r\n"
|
||||||
|
lenScratch [32]byte
|
||||||
|
|
||||||
|
// Scratch space for formatting integers and floats.
|
||||||
|
numScratch [40]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTimeout acts like Dial but takes timeouts for establishing the
|
||||||
|
// connection to the server, writing a command and reading a reply.
|
||||||
|
//
|
||||||
|
// Deprecated: Use Dial with options instead.
|
||||||
|
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
|
||||||
|
return Dial(network, address,
|
||||||
|
DialConnectTimeout(connectTimeout),
|
||||||
|
DialReadTimeout(readTimeout),
|
||||||
|
DialWriteTimeout(writeTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialOption specifies an option for dialing a Redis server.
|
||||||
|
type DialOption struct {
|
||||||
|
f func(*dialOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dialOptions struct {
|
||||||
|
readTimeout time.Duration
|
||||||
|
writeTimeout time.Duration
|
||||||
|
dial func(network, addr string) (net.Conn, error)
|
||||||
|
db int
|
||||||
|
password string
|
||||||
|
dialTLS bool
|
||||||
|
skipVerify bool
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialReadTimeout specifies the timeout for reading a single command reply.
|
||||||
|
func DialReadTimeout(d time.Duration) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.readTimeout = d
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialWriteTimeout specifies the timeout for writing a single command.
|
||||||
|
func DialWriteTimeout(d time.Duration) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.writeTimeout = d
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialConnectTimeout specifies the timeout for connecting to the Redis server.
|
||||||
|
func DialConnectTimeout(d time.Duration) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
dialer := net.Dialer{Timeout: d}
|
||||||
|
do.dial = dialer.Dial
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialNetDial specifies a custom dial function for creating TCP
|
||||||
|
// connections. If this option is left out, then net.Dial is
|
||||||
|
// used. DialNetDial overrides DialConnectTimeout.
|
||||||
|
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.dial = dial
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialDatabase specifies the database to select when dialing a connection.
|
||||||
|
func DialDatabase(db int) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.db = db
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialPassword specifies the password to use when connecting to
|
||||||
|
// the Redis server.
|
||||||
|
func DialPassword(password string) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.password = password
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTLSConfig specifies the config to use when a TLS connection is dialed.
|
||||||
|
// Has no effect when not dialing a TLS connection.
|
||||||
|
func DialTLSConfig(c *tls.Config) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.tlsConfig = c
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTLSSkipVerify to disable server name verification when connecting
|
||||||
|
// over TLS. Has no effect when not dialing a TLS connection.
|
||||||
|
func DialTLSSkipVerify(skip bool) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.skipVerify = skip
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to the Redis server at the given network and
|
||||||
|
// address using the specified options.
|
||||||
|
func Dial(network, address string, options ...DialOption) (Conn, error) {
|
||||||
|
do := dialOptions{
|
||||||
|
dial: net.Dial,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option.f(&do)
|
||||||
|
}
|
||||||
|
|
||||||
|
netConn, err := do.dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if do.dialTLS {
|
||||||
|
tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify)
|
||||||
|
if tlsConfig.ServerName == "" {
|
||||||
|
host, _, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig.ServerName = host
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(netConn, tlsConfig)
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
netConn = tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &conn{
|
||||||
|
conn: netConn,
|
||||||
|
bw: bufio.NewWriter(netConn),
|
||||||
|
br: bufio.NewReader(netConn),
|
||||||
|
readTimeout: do.readTimeout,
|
||||||
|
writeTimeout: do.writeTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
if do.password != "" {
|
||||||
|
if _, err := c.Do("AUTH", do.password); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if do.db != 0 {
|
||||||
|
if _, err := c.Do("SELECT", do.db); err != nil {
|
||||||
|
netConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialTLS(do *dialOptions) {
|
||||||
|
do.dialTLS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
|
||||||
|
|
||||||
|
// DialURL connects to a Redis server at the given URL using the Redis
|
||||||
|
// URI scheme. URLs should follow the draft IANA specification for the
|
||||||
|
// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
|
||||||
|
func DialURL(rawurl string, options ...DialOption) (Conn, error) {
|
||||||
|
u, err := url.Parse(rawurl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme != "redis" && u.Scheme != "rediss" {
|
||||||
|
return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As per the IANA draft spec, the host defaults to localhost and
|
||||||
|
// the port defaults to 6379.
|
||||||
|
host, port, err := net.SplitHostPort(u.Host)
|
||||||
|
if err != nil {
|
||||||
|
// assume port is missing
|
||||||
|
host = u.Host
|
||||||
|
port = "6379"
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
address := net.JoinHostPort(host, port)
|
||||||
|
|
||||||
|
if u.User != nil {
|
||||||
|
password, isSet := u.User.Password()
|
||||||
|
if isSet {
|
||||||
|
options = append(options, DialPassword(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match := pathDBRegexp.FindStringSubmatch(u.Path)
|
||||||
|
if len(match) == 2 {
|
||||||
|
db := 0
|
||||||
|
if len(match[1]) > 0 {
|
||||||
|
db, err = strconv.Atoi(match[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if db != 0 {
|
||||||
|
options = append(options, DialDatabase(db))
|
||||||
|
}
|
||||||
|
} else if u.Path != "" {
|
||||||
|
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "rediss" {
|
||||||
|
options = append([]DialOption{{dialTLS}}, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dial("tcp", address, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn returns a new Redigo connection for the given net connection.
|
||||||
|
func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
|
||||||
|
return &conn{
|
||||||
|
conn: netConn,
|
||||||
|
bw: bufio.NewWriter(netConn),
|
||||||
|
br: bufio.NewReader(netConn),
|
||||||
|
readTimeout: readTimeout,
|
||||||
|
writeTimeout: writeTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.err
|
||||||
|
if c.err == nil {
|
||||||
|
c.err = errors.New("redigo: closed")
|
||||||
|
err = c.conn.Close()
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) fatal(err error) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.err == nil {
|
||||||
|
c.err = err
|
||||||
|
// Close connection to force errors on subsequent calls and to unblock
|
||||||
|
// other reader or writer.
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Err() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.err
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeLen(prefix byte, n int) error {
|
||||||
|
c.lenScratch[len(c.lenScratch)-1] = '\n'
|
||||||
|
c.lenScratch[len(c.lenScratch)-2] = '\r'
|
||||||
|
i := len(c.lenScratch) - 3
|
||||||
|
for {
|
||||||
|
c.lenScratch[i] = byte('0' + n%10)
|
||||||
|
i -= 1
|
||||||
|
n = n / 10
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.lenScratch[i] = prefix
|
||||||
|
_, err := c.bw.Write(c.lenScratch[i:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeString(s string) error {
|
||||||
|
c.writeLen('$', len(s))
|
||||||
|
c.bw.WriteString(s)
|
||||||
|
_, err := c.bw.WriteString("\r\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeBytes(p []byte) error {
|
||||||
|
c.writeLen('$', len(p))
|
||||||
|
c.bw.Write(p)
|
||||||
|
_, err := c.bw.WriteString("\r\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeInt64(n int64) error {
|
||||||
|
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeFloat64(n float64) error {
|
||||||
|
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeCommand(cmd string, args []interface{}) (err error) {
|
||||||
|
c.writeLen('*', 1+len(args))
|
||||||
|
err = c.writeString(cmd)
|
||||||
|
for _, arg := range args {
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch arg := arg.(type) {
|
||||||
|
case string:
|
||||||
|
err = c.writeString(arg)
|
||||||
|
case []byte:
|
||||||
|
err = c.writeBytes(arg)
|
||||||
|
case int:
|
||||||
|
err = c.writeInt64(int64(arg))
|
||||||
|
case int64:
|
||||||
|
err = c.writeInt64(arg)
|
||||||
|
case float64:
|
||||||
|
err = c.writeFloat64(arg)
|
||||||
|
case bool:
|
||||||
|
if arg {
|
||||||
|
err = c.writeString("1")
|
||||||
|
} else {
|
||||||
|
err = c.writeString("0")
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
err = c.writeString("")
|
||||||
|
default:
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprint(&buf, arg)
|
||||||
|
err = c.writeBytes(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type protocolError string
|
||||||
|
|
||||||
|
func (pe protocolError) Error() string {
|
||||||
|
return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) readLine() ([]byte, error) {
|
||||||
|
p, err := c.br.ReadSlice('\n')
|
||||||
|
if err == bufio.ErrBufferFull {
|
||||||
|
return nil, protocolError("long response line")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i := len(p) - 2
|
||||||
|
if i < 0 || p[i] != '\r' {
|
||||||
|
return nil, protocolError("bad response line terminator")
|
||||||
|
}
|
||||||
|
return p[:i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLen parses bulk string and array lengths.
|
||||||
|
func parseLen(p []byte) (int, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return -1, protocolError("malformed length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
|
||||||
|
// handle $-1 and $-1 null replies.
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for _, b := range p {
|
||||||
|
n *= 10
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return -1, protocolError("illegal bytes in length")
|
||||||
|
}
|
||||||
|
n += int(b - '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt parses an integer reply.
|
||||||
|
func parseInt(p []byte) (interface{}, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, protocolError("malformed integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var negate bool
|
||||||
|
if p[0] == '-' {
|
||||||
|
negate = true
|
||||||
|
p = p[1:]
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, protocolError("malformed integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int64
|
||||||
|
for _, b := range p {
|
||||||
|
n *= 10
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return 0, protocolError("illegal bytes in length")
|
||||||
|
}
|
||||||
|
n += int64(b - '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if negate {
|
||||||
|
n = -n
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
okReply interface{} = "OK"
|
||||||
|
pongReply interface{} = "PONG"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *conn) readReply() (interface{}, error) {
|
||||||
|
line, err := c.readLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, protocolError("short response line")
|
||||||
|
}
|
||||||
|
switch line[0] {
|
||||||
|
case '+':
|
||||||
|
switch {
|
||||||
|
case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
|
||||||
|
// Avoid allocation for frequent "+OK" response.
|
||||||
|
return okReply, nil
|
||||||
|
case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
|
||||||
|
// Avoid allocation in PING command benchmarks :)
|
||||||
|
return pongReply, nil
|
||||||
|
default:
|
||||||
|
return string(line[1:]), nil
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
return Error(string(line[1:])), nil
|
||||||
|
case ':':
|
||||||
|
return parseInt(line[1:])
|
||||||
|
case '$':
|
||||||
|
n, err := parseLen(line[1:])
|
||||||
|
if n < 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := make([]byte, n)
|
||||||
|
_, err = io.ReadFull(c.br, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if line, err := c.readLine(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(line) != 0 {
|
||||||
|
return nil, protocolError("bad bulk string format")
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
case '*':
|
||||||
|
n, err := parseLen(line[1:])
|
||||||
|
if n < 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r := make([]interface{}, n)
|
||||||
|
for i := range r {
|
||||||
|
r[i], err = c.readReply()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, protocolError("unexpected response line")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Send(cmd string, args ...interface{}) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.pending += 1
|
||||||
|
c.mu.Unlock()
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
if err := c.writeCommand(cmd, args); err != nil {
|
||||||
|
return c.fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Flush() error {
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return c.fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Receive() (reply interface{}, err error) {
|
||||||
|
if c.readTimeout != 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||||
|
}
|
||||||
|
if reply, err = c.readReply(); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
// When using pub/sub, the number of receives can be greater than the
|
||||||
|
// number of sends. To enable normal use of the connection after
|
||||||
|
// unsubscribing from all channels, we do not decrement pending to a
|
||||||
|
// negative value.
|
||||||
|
//
|
||||||
|
// The pending field is decremented after the reply is read to handle the
|
||||||
|
// case where Receive is called before Send.
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.pending > 0 {
|
||||||
|
c.pending -= 1
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
if err, ok := reply.(Error); ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
pending := c.pending
|
||||||
|
c.pending = 0
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if cmd == "" && pending == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != "" {
|
||||||
|
if err := c.writeCommand(cmd, args); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.readTimeout != 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd == "" {
|
||||||
|
reply := make([]interface{}, pending)
|
||||||
|
for i := range reply {
|
||||||
|
r, e := c.readReply()
|
||||||
|
if e != nil {
|
||||||
|
return nil, c.fatal(e)
|
||||||
|
}
|
||||||
|
reply[i] = r
|
||||||
|
}
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var reply interface{}
|
||||||
|
for i := 0; i <= pending; i++ {
|
||||||
|
var e error
|
||||||
|
if reply, e = c.readReply(); e != nil {
|
||||||
|
return nil, c.fatal(e)
|
||||||
|
}
|
||||||
|
if e, ok := reply.(Error); ok && err == nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reply, err
|
||||||
|
}
|
670
vendor/github.com/garyburd/redigo/redis/conn_test.go
generated
vendored
Normal file
670
vendor/github.com/garyburd/redigo/redis/conn_test.go
generated
vendored
Normal file
@ -0,0 +1,670 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testConn struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testConn) Close() error { return nil }
|
||||||
|
func (*testConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (*testConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (*testConn) SetDeadline(t time.Time) error { return nil }
|
||||||
|
func (*testConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
|
func (*testConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
func dialTestConn(r io.Reader, w io.Writer) redis.DialOption {
|
||||||
|
return redis.DialNetDial(func(net, addr string) (net.Conn, error) {
|
||||||
|
return &testConn{Reader: r, Writer: w}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeTests = []struct {
|
||||||
|
args []interface{}
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", "value"},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", "value"},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", byte(100)},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", 100},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", int64(math.MinInt64)},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$20\r\n-9223372036854775808\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", float64(1349673917.939762)},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$21\r\n1.349673917939762e+09\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", ""},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "key", nil},
|
||||||
|
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"ECHO", true, false},
|
||||||
|
"*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrite(t *testing.T) {
|
||||||
|
for _, tt := range writeTests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
c, _ := redis.Dial("", "", dialTestConn(nil, &buf))
|
||||||
|
err := c.Send(tt.args[0].(string), tt.args[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Send(%v) returned error %v", tt.args, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.Flush()
|
||||||
|
actual := buf.String()
|
||||||
|
if actual != tt.expected {
|
||||||
|
t.Errorf("Send(%v) = %q, want %q", tt.args, actual, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorSentinel = &struct{}{}
|
||||||
|
|
||||||
|
var readTests = []struct {
|
||||||
|
reply string
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"+OK\r\n",
|
||||||
|
"OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"+PONG\r\n",
|
||||||
|
"PONG",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@OK\r\n",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$6\r\nfoobar\r\n",
|
||||||
|
[]byte("foobar"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$-1\r\n",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
":1\r\n",
|
||||||
|
int64(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
":-2\r\n",
|
||||||
|
int64(-2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"*0\r\n",
|
||||||
|
[]interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"*-1\r\n",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"*4\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nHello\r\n$5\r\nWorld\r\n",
|
||||||
|
[]interface{}{[]byte("foo"), []byte("bar"), []byte("Hello"), []byte("World")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n",
|
||||||
|
[]interface{}{[]byte("foo"), nil, []byte("bar")},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// "x" is not a valid length
|
||||||
|
"$x\r\nfoobar\r\n",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// -2 is not a valid length
|
||||||
|
"$-2\r\n",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// "x" is not a valid integer
|
||||||
|
":x\r\n",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// missing \r\n following value
|
||||||
|
"$6\r\nfoobar",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// short value
|
||||||
|
"$6\r\nxx",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// long value
|
||||||
|
"$6\r\nfoobarx\r\n",
|
||||||
|
errorSentinel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRead(t *testing.T) {
|
||||||
|
for _, tt := range readTests {
|
||||||
|
c, _ := redis.Dial("", "", dialTestConn(strings.NewReader(tt.reply), nil))
|
||||||
|
actual, err := c.Receive()
|
||||||
|
if tt.expected == errorSentinel {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Receive(%q) did not return expected error", tt.reply)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Receive(%q) returned error %v", tt.reply, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(actual, tt.expected) {
|
||||||
|
t.Errorf("Receive(%q) = %v, want %v", tt.reply, actual, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testCommands = []struct {
|
||||||
|
args []interface{}
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]interface{}{"PING"},
|
||||||
|
"PONG",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"SET", "foo", "bar"},
|
||||||
|
"OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"GET", "foo"},
|
||||||
|
[]byte("bar"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"GET", "nokey"},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"MGET", "nokey", "foo"},
|
||||||
|
[]interface{}{nil, []byte("bar")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"INCR", "mycounter"},
|
||||||
|
int64(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"LPUSH", "mylist", "foo"},
|
||||||
|
int64(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"LPUSH", "mylist", "bar"},
|
||||||
|
int64(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"LRANGE", "mylist", 0, -1},
|
||||||
|
[]interface{}{[]byte("bar"), []byte("foo")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"MULTI"},
|
||||||
|
"OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"LRANGE", "mylist", 0, -1},
|
||||||
|
"QUEUED",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"PING"},
|
||||||
|
"QUEUED",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{"EXEC"},
|
||||||
|
[]interface{}{
|
||||||
|
[]interface{}{[]byte("bar"), []byte("foo")},
|
||||||
|
"PONG",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoCommands(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
for _, cmd := range testCommands {
|
||||||
|
actual, err := c.Do(cmd.args[0].(string), cmd.args[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Do(%v) returned error %v", cmd.args, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(actual, cmd.expected) {
|
||||||
|
t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineCommands(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
for _, cmd := range testCommands {
|
||||||
|
if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil {
|
||||||
|
t.Fatalf("Send(%v) returned error %v", cmd.args, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.Flush(); err != nil {
|
||||||
|
t.Errorf("Flush() returned error %v", err)
|
||||||
|
}
|
||||||
|
for _, cmd := range testCommands {
|
||||||
|
actual, err := c.Receive()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Receive(%v) returned error %v", cmd.args, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(actual, cmd.expected) {
|
||||||
|
t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlankCommmand(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
for _, cmd := range testCommands {
|
||||||
|
if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil {
|
||||||
|
t.Fatalf("Send(%v) returned error %v", cmd.args, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply, err := redis.Values(c.Do(""))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Do() returned error %v", err)
|
||||||
|
}
|
||||||
|
if len(reply) != len(testCommands) {
|
||||||
|
t.Fatalf("len(reply)=%d, want %d", len(reply), len(testCommands))
|
||||||
|
}
|
||||||
|
for i, cmd := range testCommands {
|
||||||
|
actual := reply[i]
|
||||||
|
if !reflect.DeepEqual(actual, cmd.expected) {
|
||||||
|
t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecvBeforeSend(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
c.Receive()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
c.Send("PING")
|
||||||
|
c.Flush()
|
||||||
|
<-done
|
||||||
|
_, err = c.Do("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Do("SET", "key", "val")
|
||||||
|
_, err = c.Do("HSET", "key", "fld", "val")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected err for HSET on string key.")
|
||||||
|
}
|
||||||
|
if c.Err() != nil {
|
||||||
|
t.Errorf("Conn has Err()=%v, expect nil", c.Err())
|
||||||
|
}
|
||||||
|
_, err = c.Do("SET", "key", "val")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Do(SET, key, val) returned error %v, expected nil.", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadTimeout(t *testing.T) {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("net.Listen returned %v", err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
c.Write([]byte("+OK\r\n"))
|
||||||
|
c.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Do
|
||||||
|
|
||||||
|
c1, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("redis.Dial returned %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
|
||||||
|
_, err = c1.Do("PING")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("c1.Do() returned nil, expect error")
|
||||||
|
}
|
||||||
|
if c1.Err() == nil {
|
||||||
|
t.Fatalf("c1.Err() = nil, expect error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send/Flush/Receive
|
||||||
|
|
||||||
|
c2, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("redis.Dial returned %v", err)
|
||||||
|
}
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
c2.Send("PING")
|
||||||
|
c2.Flush()
|
||||||
|
_, err = c2.Receive()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("c2.Receive() returned nil, expect error")
|
||||||
|
}
|
||||||
|
if c2.Err() == nil {
|
||||||
|
t.Fatalf("c2.Err() = nil, expect error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialErrors = []struct {
|
||||||
|
rawurl string
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"localhost",
|
||||||
|
"invalid redis URL scheme",
|
||||||
|
},
|
||||||
|
// The error message for invalid hosts is diffferent in different
|
||||||
|
// versions of Go, so just check that there is an error message.
|
||||||
|
{
|
||||||
|
"redis://weird url",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"redis://foo:bar:baz",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"http://www.google.com",
|
||||||
|
"invalid redis URL scheme: http",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"redis://localhost:6379/abc123",
|
||||||
|
"invalid database: abc123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialURLErrors(t *testing.T) {
|
||||||
|
for _, d := range dialErrors {
|
||||||
|
_, err := redis.DialURL(d.rawurl)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), d.expectedError) {
|
||||||
|
t.Errorf("DialURL did not return expected error (expected %v to contain %s)", err, d.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialURLPort(t *testing.T) {
|
||||||
|
checkPort := func(network, address string) (net.Conn, error) {
|
||||||
|
if address != "localhost:6379" {
|
||||||
|
t.Errorf("DialURL did not set port to 6379 by default (got %v)", address)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
_, err := redis.DialURL("redis://localhost", redis.DialNetDial(checkPort))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("dial error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialURLHost(t *testing.T) {
|
||||||
|
checkHost := func(network, address string) (net.Conn, error) {
|
||||||
|
if address != "localhost:6379" {
|
||||||
|
t.Errorf("DialURL did not set host to localhost by default (got %v)", address)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
_, err := redis.DialURL("redis://:6379", redis.DialNetDial(checkHost))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("dial error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialURLPassword(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err := redis.DialURL("redis://x:abc123@localhost", dialTestConn(strings.NewReader("+OK\r\n"), &buf))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("dial error:", err)
|
||||||
|
}
|
||||||
|
expected := "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"
|
||||||
|
actual := buf.String()
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("commands = %q, want %q", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialURLDatabase(t *testing.T) {
|
||||||
|
var buf3 bytes.Buffer
|
||||||
|
_, err3 := redis.DialURL("redis://localhost/3", dialTestConn(strings.NewReader("+OK\r\n"), &buf3))
|
||||||
|
if err3 != nil {
|
||||||
|
t.Error("dial error:", err3)
|
||||||
|
}
|
||||||
|
expected3 := "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"
|
||||||
|
actual3 := buf3.String()
|
||||||
|
if actual3 != expected3 {
|
||||||
|
t.Errorf("commands = %q, want %q", actual3, expected3)
|
||||||
|
}
|
||||||
|
// empty DB means 0
|
||||||
|
var buf0 bytes.Buffer
|
||||||
|
_, err0 := redis.DialURL("redis://localhost/", dialTestConn(strings.NewReader("+OK\r\n"), &buf0))
|
||||||
|
if err0 != nil {
|
||||||
|
t.Error("dial error:", err0)
|
||||||
|
}
|
||||||
|
expected0 := ""
|
||||||
|
actual0 := buf0.String()
|
||||||
|
if actual0 != expected0 {
|
||||||
|
t.Errorf("commands = %q, want %q", actual0, expected0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to local instance of Redis running on the default port.
|
||||||
|
func ExampleDial() {
|
||||||
|
c, err := redis.Dial("tcp", ":6379")
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to remote instance of Redis using a URL.
|
||||||
|
func ExampleDialURL() {
|
||||||
|
c, err := redis.DialURL(os.Getenv("REDIS_URL"))
|
||||||
|
if err != nil {
|
||||||
|
// handle connection error
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextExecError tests handling of errors in a transaction. See
|
||||||
|
// http://redis.io/topics/transactions for information on how Redis handles
|
||||||
|
// errors in a transaction.
|
||||||
|
func TestExecError(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// Execute commands that fail before EXEC is called.
|
||||||
|
|
||||||
|
c.Do("DEL", "k0")
|
||||||
|
c.Do("ZADD", "k0", 0, 0)
|
||||||
|
c.Send("MULTI")
|
||||||
|
c.Send("NOTACOMMAND", "k0", 0, 0)
|
||||||
|
c.Send("ZINCRBY", "k0", 0, 0)
|
||||||
|
v, err := c.Do("EXEC")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("EXEC returned values %v, expected error", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute commands that fail after EXEC is called. The first command
|
||||||
|
// returns an error.
|
||||||
|
|
||||||
|
c.Do("DEL", "k1")
|
||||||
|
c.Do("ZADD", "k1", 0, 0)
|
||||||
|
c.Send("MULTI")
|
||||||
|
c.Send("HSET", "k1", 0, 0)
|
||||||
|
c.Send("ZINCRBY", "k1", 0, 0)
|
||||||
|
v, err = c.Do("EXEC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EXEC returned error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vs, err := redis.Values(v, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Values(v) returned error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vs) != 2 {
|
||||||
|
t.Fatalf("len(vs) == %d, want 2", len(vs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := vs[0].(error); !ok {
|
||||||
|
t.Fatalf("first result is type %T, expected error", vs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := vs[1].([]byte); !ok {
|
||||||
|
t.Fatalf("second result is type %T, expected []byte", vs[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute commands that fail after EXEC is called. The second command
|
||||||
|
// returns an error.
|
||||||
|
|
||||||
|
c.Do("ZADD", "k2", 0, 0)
|
||||||
|
c.Send("MULTI")
|
||||||
|
c.Send("ZINCRBY", "k2", 0, 0)
|
||||||
|
c.Send("HSET", "k2", 0, 0)
|
||||||
|
v, err = c.Do("EXEC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EXEC returned error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vs, err = redis.Values(v, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Values(v) returned error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vs) != 2 {
|
||||||
|
t.Fatalf("len(vs) == %d, want 2", len(vs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := vs[0].([]byte); !ok {
|
||||||
|
t.Fatalf("first result is type %T, expected []byte", vs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := vs[1].(error); !ok {
|
||||||
|
t.Fatalf("second result is type %T, expected error", vs[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDoEmpty(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := c.Do(""); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDoPing(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
177
vendor/github.com/garyburd/redigo/redis/doc.go
generated
vendored
Normal file
177
vendor/github.com/garyburd/redigo/redis/doc.go
generated
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
// Package redis is a client for the Redis database.
|
||||||
|
//
|
||||||
|
// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more
|
||||||
|
// documentation about this package.
|
||||||
|
//
|
||||||
|
// Connections
|
||||||
|
//
|
||||||
|
// The Conn interface is the primary interface for working with Redis.
|
||||||
|
// Applications create connections by calling the Dial, DialWithTimeout or
|
||||||
|
// NewConn functions. In the future, functions will be added for creating
|
||||||
|
// sharded and other types of connections.
|
||||||
|
//
|
||||||
|
// The application must call the connection Close method when the application
|
||||||
|
// is done with the connection.
|
||||||
|
//
|
||||||
|
// Executing Commands
|
||||||
|
//
|
||||||
|
// The Conn interface has a generic method for executing Redis commands:
|
||||||
|
//
|
||||||
|
// Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||||
|
//
|
||||||
|
// The Redis command reference (http://redis.io/commands) lists the available
|
||||||
|
// commands. An example of using the Redis APPEND command is:
|
||||||
|
//
|
||||||
|
// n, err := conn.Do("APPEND", "key", "value")
|
||||||
|
//
|
||||||
|
// The Do method converts command arguments to binary strings for transmission
|
||||||
|
// to the server as follows:
|
||||||
|
//
|
||||||
|
// Go Type Conversion
|
||||||
|
// []byte Sent as is
|
||||||
|
// string Sent as is
|
||||||
|
// int, int64 strconv.FormatInt(v)
|
||||||
|
// float64 strconv.FormatFloat(v, 'g', -1, 64)
|
||||||
|
// bool true -> "1", false -> "0"
|
||||||
|
// nil ""
|
||||||
|
// all other types fmt.Print(v)
|
||||||
|
//
|
||||||
|
// Redis command reply types are represented using the following Go types:
|
||||||
|
//
|
||||||
|
// Redis type Go type
|
||||||
|
// error redis.Error
|
||||||
|
// integer int64
|
||||||
|
// simple string string
|
||||||
|
// bulk string []byte or nil if value not present.
|
||||||
|
// array []interface{} or nil if value not present.
|
||||||
|
//
|
||||||
|
// Use type assertions or the reply helper functions to convert from
|
||||||
|
// interface{} to the specific Go type for the command result.
|
||||||
|
//
|
||||||
|
// Pipelining
|
||||||
|
//
|
||||||
|
// Connections support pipelining using the Send, Flush and Receive methods.
|
||||||
|
//
|
||||||
|
// Send(commandName string, args ...interface{}) error
|
||||||
|
// Flush() error
|
||||||
|
// Receive() (reply interface{}, err error)
|
||||||
|
//
|
||||||
|
// Send writes the command to the connection's output buffer. Flush flushes the
|
||||||
|
// connection's output buffer to the server. Receive reads a single reply from
|
||||||
|
// the server. The following example shows a simple pipeline.
|
||||||
|
//
|
||||||
|
// c.Send("SET", "foo", "bar")
|
||||||
|
// c.Send("GET", "foo")
|
||||||
|
// c.Flush()
|
||||||
|
// c.Receive() // reply from SET
|
||||||
|
// v, err = c.Receive() // reply from GET
|
||||||
|
//
|
||||||
|
// The Do method combines the functionality of the Send, Flush and Receive
|
||||||
|
// methods. The Do method starts by writing the command and flushing the output
|
||||||
|
// buffer. Next, the Do method receives all pending replies including the reply
|
||||||
|
// for the command just sent by Do. If any of the received replies is an error,
|
||||||
|
// then Do returns the error. If there are no errors, then Do returns the last
|
||||||
|
// reply. If the command argument to the Do method is "", then the Do method
|
||||||
|
// will flush the output buffer and receive pending replies without sending a
|
||||||
|
// command.
|
||||||
|
//
|
||||||
|
// Use the Send and Do methods to implement pipelined transactions.
|
||||||
|
//
|
||||||
|
// c.Send("MULTI")
|
||||||
|
// c.Send("INCR", "foo")
|
||||||
|
// c.Send("INCR", "bar")
|
||||||
|
// r, err := c.Do("EXEC")
|
||||||
|
// fmt.Println(r) // prints [1, 1]
|
||||||
|
//
|
||||||
|
// Concurrency
|
||||||
|
//
|
||||||
|
// Connections support one concurrent caller to the Receive method and one
|
||||||
|
// concurrent caller to the Send and Flush methods. No other concurrency is
|
||||||
|
// supported including concurrent calls to the Do method.
|
||||||
|
//
|
||||||
|
// For full concurrent access to Redis, use the thread-safe Pool to get, use
|
||||||
|
// and release a connection from within a goroutine. Connections returned from
|
||||||
|
// a Pool have the concurrency restrictions described in the previous
|
||||||
|
// paragraph.
|
||||||
|
//
|
||||||
|
// Publish and Subscribe
|
||||||
|
//
|
||||||
|
// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
|
||||||
|
//
|
||||||
|
// c.Send("SUBSCRIBE", "example")
|
||||||
|
// c.Flush()
|
||||||
|
// for {
|
||||||
|
// reply, err := c.Receive()
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// // process pushed message
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The PubSubConn type wraps a Conn with convenience methods for implementing
|
||||||
|
// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
|
||||||
|
// send and flush a subscription management command. The receive method
|
||||||
|
// converts a pushed message to convenient types for use in a type switch.
|
||||||
|
//
|
||||||
|
// psc := redis.PubSubConn{Conn: c}
|
||||||
|
// psc.Subscribe("example")
|
||||||
|
// for {
|
||||||
|
// switch v := psc.Receive().(type) {
|
||||||
|
// case redis.Message:
|
||||||
|
// fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
|
||||||
|
// case redis.Subscription:
|
||||||
|
// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
|
||||||
|
// case error:
|
||||||
|
// return v
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Reply Helpers
|
||||||
|
//
|
||||||
|
// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
|
||||||
|
// to a value of a specific type. To allow convenient wrapping of calls to the
|
||||||
|
// connection Do and Receive methods, the functions take a second argument of
|
||||||
|
// type error. If the error is non-nil, then the helper function returns the
|
||||||
|
// error. If the error is nil, the function converts the reply to the specified
|
||||||
|
// type:
|
||||||
|
//
|
||||||
|
// exists, err := redis.Bool(c.Do("EXISTS", "foo"))
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error return from c.Do or type conversion error.
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The Scan function converts elements of a array reply to Go types:
|
||||||
|
//
|
||||||
|
// var value1 int
|
||||||
|
// var value2 string
|
||||||
|
// reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
// if _, err := redis.Scan(reply, &value1, &value2); err != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Errors
|
||||||
|
//
|
||||||
|
// Connection methods return error replies from the server as type redis.Error.
|
||||||
|
//
|
||||||
|
// Call the connection Err() method to determine if the connection encountered
|
||||||
|
// non-recoverable error such as a network error or protocol parsing error. If
|
||||||
|
// Err() returns a non-nil value, then the connection is not usable and should
|
||||||
|
// be closed.
|
||||||
|
package redis // import "github.com/garyburd/redigo/redis"
|
33
vendor/github.com/garyburd/redigo/redis/go17.go
generated
vendored
Normal file
33
vendor/github.com/garyburd/redigo/redis/go17.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// +build go1.7
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import "crypto/tls"
|
||||||
|
|
||||||
|
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
|
||||||
|
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
|
||||||
|
if cfg == nil {
|
||||||
|
return &tls.Config{InsecureSkipVerify: skipVerify}
|
||||||
|
}
|
||||||
|
return &tls.Config{
|
||||||
|
Rand: cfg.Rand,
|
||||||
|
Time: cfg.Time,
|
||||||
|
Certificates: cfg.Certificates,
|
||||||
|
NameToCertificate: cfg.NameToCertificate,
|
||||||
|
GetCertificate: cfg.GetCertificate,
|
||||||
|
RootCAs: cfg.RootCAs,
|
||||||
|
NextProtos: cfg.NextProtos,
|
||||||
|
ServerName: cfg.ServerName,
|
||||||
|
ClientAuth: cfg.ClientAuth,
|
||||||
|
ClientCAs: cfg.ClientCAs,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
|
CipherSuites: cfg.CipherSuites,
|
||||||
|
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||||
|
ClientSessionCache: cfg.ClientSessionCache,
|
||||||
|
MinVersion: cfg.MinVersion,
|
||||||
|
MaxVersion: cfg.MaxVersion,
|
||||||
|
CurvePreferences: cfg.CurvePreferences,
|
||||||
|
DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled,
|
||||||
|
Renegotiation: cfg.Renegotiation,
|
||||||
|
}
|
||||||
|
}
|
117
vendor/github.com/garyburd/redigo/redis/log.go
generated
vendored
Normal file
117
vendor/github.com/garyburd/redigo/redis/log.go
generated
vendored
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLoggingConn returns a logging wrapper around a connection.
|
||||||
|
func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = prefix + "."
|
||||||
|
}
|
||||||
|
return &loggingConn{conn, logger, prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggingConn struct {
|
||||||
|
Conn
|
||||||
|
logger *log.Logger
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Close() error {
|
||||||
|
err := c.Conn.Close()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
|
||||||
|
c.logger.Output(2, buf.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
|
||||||
|
const chop = 32
|
||||||
|
switch v := v.(type) {
|
||||||
|
case []byte:
|
||||||
|
if len(v) > chop {
|
||||||
|
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%q", v)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if len(v) > chop {
|
||||||
|
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%q", v)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
if len(v) == 0 {
|
||||||
|
buf.WriteString("[]")
|
||||||
|
} else {
|
||||||
|
sep := "["
|
||||||
|
fin := "]"
|
||||||
|
if len(v) > chop {
|
||||||
|
v = v[:chop]
|
||||||
|
fin = "...]"
|
||||||
|
}
|
||||||
|
for _, vv := range v {
|
||||||
|
buf.WriteString(sep)
|
||||||
|
c.printValue(buf, vv)
|
||||||
|
sep = ", "
|
||||||
|
}
|
||||||
|
buf.WriteString(fin)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Fprint(buf, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
|
||||||
|
if method != "Receive" {
|
||||||
|
buf.WriteString(commandName)
|
||||||
|
for _, arg := range args {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
c.printValue(&buf, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString(") -> (")
|
||||||
|
if method != "Send" {
|
||||||
|
c.printValue(&buf, reply)
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "%v)", err)
|
||||||
|
c.logger.Output(3, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
|
||||||
|
reply, err := c.Conn.Do(commandName, args...)
|
||||||
|
c.print("Do", commandName, args, reply, err)
|
||||||
|
return reply, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
|
||||||
|
err := c.Conn.Send(commandName, args...)
|
||||||
|
c.print("Send", commandName, args, nil, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Receive() (interface{}, error) {
|
||||||
|
reply, err := c.Conn.Receive()
|
||||||
|
c.print("Receive", "", nil, reply, err)
|
||||||
|
return reply, err
|
||||||
|
}
|
416
vendor/github.com/garyburd/redigo/redis/pool.go
generated
vendored
Normal file
416
vendor/github.com/garyburd/redigo/redis/pool.go
generated
vendored
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nowFunc = time.Now // for testing
|
||||||
|
|
||||||
|
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
|
||||||
|
// Receive, Flush, Err) when the maximum number of database connections in the
|
||||||
|
// pool has been reached.
|
||||||
|
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
|
||||||
|
|
||||||
|
var (
|
||||||
|
errPoolClosed = errors.New("redigo: connection pool closed")
|
||||||
|
errConnClosed = errors.New("redigo: connection closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pool maintains a pool of connections. The application calls the Get method
|
||||||
|
// to get a connection from the pool and the connection's Close method to
|
||||||
|
// return the connection's resources to the pool.
|
||||||
|
//
|
||||||
|
// The following example shows how to use a pool in a web application. The
|
||||||
|
// application creates a pool at application startup and makes it available to
|
||||||
|
// request handlers using a package level variable. The pool configuration used
|
||||||
|
// here is an example, not a recommendation.
|
||||||
|
//
|
||||||
|
// func newPool(addr string) *redis.Pool {
|
||||||
|
// return &redis.Pool{
|
||||||
|
// MaxIdle: 3,
|
||||||
|
// IdleTimeout: 240 * time.Second,
|
||||||
|
// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var (
|
||||||
|
// pool *redis.Pool
|
||||||
|
// redisServer = flag.String("redisServer", ":6379", "")
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// flag.Parse()
|
||||||
|
// pool = newPool(*redisServer)
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// A request handler gets a connection from the pool and closes the connection
|
||||||
|
// when the handler is done:
|
||||||
|
//
|
||||||
|
// func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// conn := pool.Get()
|
||||||
|
// defer conn.Close()
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use the Dial function to authenticate connections with the AUTH command or
|
||||||
|
// select a database with the SELECT command:
|
||||||
|
//
|
||||||
|
// pool := &redis.Pool{
|
||||||
|
// // Other pool configuration not shown in this example.
|
||||||
|
// Dial: func () (redis.Conn, error) {
|
||||||
|
// c, err := redis.Dial("tcp", server)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if _, err := c.Do("AUTH", password); err != nil {
|
||||||
|
// c.Close()
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if _, err := c.Do("SELECT", db); err != nil {
|
||||||
|
// c.Close()
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return c, nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use the TestOnBorrow function to check the health of an idle connection
|
||||||
|
// before the connection is returned to the application. This example PINGs
|
||||||
|
// connections that have been idle more than a minute:
|
||||||
|
//
|
||||||
|
// pool := &redis.Pool{
|
||||||
|
// // Other pool configuration not shown in this example.
|
||||||
|
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
// if time.Since(t) < time.Minute {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// _, err := c.Do("PING")
|
||||||
|
// return err
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
type Pool struct {
|
||||||
|
|
||||||
|
// Dial is an application supplied function for creating and configuring a
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// The connection returned from Dial must not be in a special state
|
||||||
|
// (subscribed to pubsub channel, transaction started, ...).
|
||||||
|
Dial func() (Conn, error)
|
||||||
|
|
||||||
|
// TestOnBorrow is an optional application supplied function for checking
|
||||||
|
// the health of an idle connection before the connection is used again by
|
||||||
|
// the application. Argument t is the time that the connection was returned
|
||||||
|
// to the pool. If the function returns an error, then the connection is
|
||||||
|
// closed.
|
||||||
|
TestOnBorrow func(c Conn, t time.Time) error
|
||||||
|
|
||||||
|
// Maximum number of idle connections in the pool.
|
||||||
|
MaxIdle int
|
||||||
|
|
||||||
|
// Maximum number of connections allocated by the pool at a given time.
|
||||||
|
// When zero, there is no limit on the number of connections in the pool.
|
||||||
|
MaxActive int
|
||||||
|
|
||||||
|
// Close connections after remaining idle for this duration. If the value
|
||||||
|
// is zero, then idle connections are not closed. Applications should set
|
||||||
|
// the timeout to a value less than the server's timeout.
|
||||||
|
IdleTimeout time.Duration
|
||||||
|
|
||||||
|
// If Wait is true and the pool is at the MaxActive limit, then Get() waits
|
||||||
|
// for a connection to be returned to the pool before returning.
|
||||||
|
Wait bool
|
||||||
|
|
||||||
|
// mu protects fields defined below.
|
||||||
|
mu sync.Mutex
|
||||||
|
cond *sync.Cond
|
||||||
|
closed bool
|
||||||
|
active int
|
||||||
|
|
||||||
|
// Stack of idleConn with most recently used at the front.
|
||||||
|
idle list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type idleConn struct {
|
||||||
|
c Conn
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool creates a new pool.
|
||||||
|
//
|
||||||
|
// Deprecated: Initialize the Pool directory as shown in the example.
|
||||||
|
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
|
||||||
|
return &Pool{Dial: newFn, MaxIdle: maxIdle}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a connection. The application must close the returned connection.
|
||||||
|
// This method always returns a valid connection so that applications can defer
|
||||||
|
// error handling to the first use of the connection. If there is an error
|
||||||
|
// getting an underlying connection, then the connection Err, Do, Send, Flush
|
||||||
|
// and Receive methods return that error.
|
||||||
|
func (p *Pool) Get() Conn {
|
||||||
|
c, err := p.get()
|
||||||
|
if err != nil {
|
||||||
|
return errorConnection{err}
|
||||||
|
}
|
||||||
|
return &pooledConnection{p: p, c: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveCount returns the number of active connections in the pool.
|
||||||
|
func (p *Pool) ActiveCount() int {
|
||||||
|
p.mu.Lock()
|
||||||
|
active := p.active
|
||||||
|
p.mu.Unlock()
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the resources used by the pool.
|
||||||
|
func (p *Pool) Close() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
idle := p.idle
|
||||||
|
p.idle.Init()
|
||||||
|
p.closed = true
|
||||||
|
p.active -= idle.Len()
|
||||||
|
if p.cond != nil {
|
||||||
|
p.cond.Broadcast()
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
for e := idle.Front(); e != nil; e = e.Next() {
|
||||||
|
e.Value.(idleConn).c.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// release decrements the active count and signals waiters. The caller must
|
||||||
|
// hold p.mu during the call.
|
||||||
|
func (p *Pool) release() {
|
||||||
|
p.active -= 1
|
||||||
|
if p.cond != nil {
|
||||||
|
p.cond.Signal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get prunes stale connections and returns a connection from the idle list or
|
||||||
|
// creates a new connection.
|
||||||
|
func (p *Pool) get() (Conn, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
|
||||||
|
// Prune stale connections.
|
||||||
|
|
||||||
|
if timeout := p.IdleTimeout; timeout > 0 {
|
||||||
|
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||||
|
e := p.idle.Back()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ic := e.Value.(idleConn)
|
||||||
|
if ic.t.Add(timeout).After(nowFunc()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.idle.Remove(e)
|
||||||
|
p.release()
|
||||||
|
p.mu.Unlock()
|
||||||
|
ic.c.Close()
|
||||||
|
p.mu.Lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
// Get idle connection.
|
||||||
|
|
||||||
|
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||||
|
e := p.idle.Front()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ic := e.Value.(idleConn)
|
||||||
|
p.idle.Remove(e)
|
||||||
|
test := p.TestOnBorrow
|
||||||
|
p.mu.Unlock()
|
||||||
|
if test == nil || test(ic.c, ic.t) == nil {
|
||||||
|
return ic.c, nil
|
||||||
|
}
|
||||||
|
ic.c.Close()
|
||||||
|
p.mu.Lock()
|
||||||
|
p.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pool closed before dialing a new connection.
|
||||||
|
|
||||||
|
if p.closed {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil, errors.New("redigo: get on closed pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial new connection if under limit.
|
||||||
|
|
||||||
|
if p.MaxActive == 0 || p.active < p.MaxActive {
|
||||||
|
dial := p.Dial
|
||||||
|
p.active += 1
|
||||||
|
p.mu.Unlock()
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.release()
|
||||||
|
p.mu.Unlock()
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Wait {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil, ErrPoolExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.cond == nil {
|
||||||
|
p.cond = sync.NewCond(&p.mu)
|
||||||
|
}
|
||||||
|
p.cond.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) put(c Conn, forceClose bool) error {
|
||||||
|
err := c.Err()
|
||||||
|
p.mu.Lock()
|
||||||
|
if !p.closed && err == nil && !forceClose {
|
||||||
|
p.idle.PushFront(idleConn{t: nowFunc(), c: c})
|
||||||
|
if p.idle.Len() > p.MaxIdle {
|
||||||
|
c = p.idle.Remove(p.idle.Back()).(idleConn).c
|
||||||
|
} else {
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
if p.cond != nil {
|
||||||
|
p.cond.Signal()
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.release()
|
||||||
|
p.mu.Unlock()
|
||||||
|
return c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type pooledConnection struct {
|
||||||
|
p *Pool
|
||||||
|
c Conn
|
||||||
|
state int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sentinel []byte
|
||||||
|
sentinelOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func initSentinel() {
|
||||||
|
p := make([]byte, 64)
|
||||||
|
if _, err := rand.Read(p); err == nil {
|
||||||
|
sentinel = p
|
||||||
|
} else {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, "Oops, rand failed. Use time instead.")
|
||||||
|
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||||
|
sentinel = h.Sum(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Close() error {
|
||||||
|
c := pc.c
|
||||||
|
if _, ok := c.(errorConnection); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pc.c = errorConnection{errConnClosed}
|
||||||
|
|
||||||
|
if pc.state&internal.MultiState != 0 {
|
||||||
|
c.Send("DISCARD")
|
||||||
|
pc.state &^= (internal.MultiState | internal.WatchState)
|
||||||
|
} else if pc.state&internal.WatchState != 0 {
|
||||||
|
c.Send("UNWATCH")
|
||||||
|
pc.state &^= internal.WatchState
|
||||||
|
}
|
||||||
|
if pc.state&internal.SubscribeState != 0 {
|
||||||
|
c.Send("UNSUBSCRIBE")
|
||||||
|
c.Send("PUNSUBSCRIBE")
|
||||||
|
// To detect the end of the message stream, ask the server to echo
|
||||||
|
// a sentinel value and read until we see that value.
|
||||||
|
sentinelOnce.Do(initSentinel)
|
||||||
|
c.Send("ECHO", sentinel)
|
||||||
|
c.Flush()
|
||||||
|
for {
|
||||||
|
p, err := c.Receive()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
|
||||||
|
pc.state &^= internal.SubscribeState
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Do("")
|
||||||
|
pc.p.put(c, pc.state != 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Err() error {
|
||||||
|
return pc.c.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||||
|
ci := internal.LookupCommandInfo(commandName)
|
||||||
|
pc.state = (pc.state | ci.Set) &^ ci.Clear
|
||||||
|
return pc.c.Do(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
|
||||||
|
ci := internal.LookupCommandInfo(commandName)
|
||||||
|
pc.state = (pc.state | ci.Set) &^ ci.Clear
|
||||||
|
return pc.c.Send(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Flush() error {
|
||||||
|
return pc.c.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *pooledConnection) Receive() (reply interface{}, err error) {
|
||||||
|
return pc.c.Receive()
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorConnection struct{ err error }
|
||||||
|
|
||||||
|
func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
|
||||||
|
func (ec errorConnection) Send(string, ...interface{}) error { return ec.err }
|
||||||
|
func (ec errorConnection) Err() error { return ec.err }
|
||||||
|
func (ec errorConnection) Close() error { return ec.err }
|
||||||
|
func (ec errorConnection) Flush() error { return ec.err }
|
||||||
|
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }
|
684
vendor/github.com/garyburd/redigo/redis/pool_test.go
generated
vendored
Normal file
684
vendor/github.com/garyburd/redigo/redis/pool_test.go
generated
vendored
Normal file
@ -0,0 +1,684 @@
|
|||||||
|
// Copyright 2011 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type poolTestConn struct {
|
||||||
|
d *poolDialer
|
||||||
|
err error
|
||||||
|
redis.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *poolTestConn) Close() error {
|
||||||
|
c.d.mu.Lock()
|
||||||
|
c.d.open -= 1
|
||||||
|
c.d.mu.Unlock()
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *poolTestConn) Err() error { return c.err }
|
||||||
|
|
||||||
|
func (c *poolTestConn) Do(commandName string, args ...interface{}) (interface{}, error) {
|
||||||
|
if commandName == "ERR" {
|
||||||
|
c.err = args[0].(error)
|
||||||
|
commandName = "PING"
|
||||||
|
}
|
||||||
|
if commandName != "" {
|
||||||
|
c.d.commands = append(c.d.commands, commandName)
|
||||||
|
}
|
||||||
|
return c.Conn.Do(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *poolTestConn) Send(commandName string, args ...interface{}) error {
|
||||||
|
c.d.commands = append(c.d.commands, commandName)
|
||||||
|
return c.Conn.Send(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type poolDialer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
t *testing.T
|
||||||
|
dialed int
|
||||||
|
open int
|
||||||
|
commands []string
|
||||||
|
dialErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *poolDialer) dial() (redis.Conn, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
d.dialed += 1
|
||||||
|
dialErr := d.dialErr
|
||||||
|
d.mu.Unlock()
|
||||||
|
if dialErr != nil {
|
||||||
|
return nil, d.dialErr
|
||||||
|
}
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d.mu.Lock()
|
||||||
|
d.open += 1
|
||||||
|
d.mu.Unlock()
|
||||||
|
return &poolTestConn{d: d, Conn: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *poolDialer) check(message string, p *redis.Pool, dialed, open int) {
|
||||||
|
d.mu.Lock()
|
||||||
|
if d.dialed != dialed {
|
||||||
|
d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed)
|
||||||
|
}
|
||||||
|
if d.open != open {
|
||||||
|
d.t.Errorf("%s: open=%d, want %d", message, d.open, open)
|
||||||
|
}
|
||||||
|
if active := p.ActiveCount(); active != open {
|
||||||
|
d.t.Errorf("%s: active=%d, want %d", message, active, open)
|
||||||
|
}
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolReuse(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c1 := p.Get()
|
||||||
|
c1.Do("PING")
|
||||||
|
c2 := p.Get()
|
||||||
|
c2.Do("PING")
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
d.check("before close", p, 2, 2)
|
||||||
|
p.Close()
|
||||||
|
d.check("after close", p, 2, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMaxIdle(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c1 := p.Get()
|
||||||
|
c1.Do("PING")
|
||||||
|
c2 := p.Get()
|
||||||
|
c2.Do("PING")
|
||||||
|
c3 := p.Get()
|
||||||
|
c3.Do("PING")
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
c3.Close()
|
||||||
|
}
|
||||||
|
d.check("before close", p, 12, 2)
|
||||||
|
p.Close()
|
||||||
|
d.check("after close", p, 12, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolError(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
c.Do("ERR", io.EOF)
|
||||||
|
if c.Err() == nil {
|
||||||
|
t.Errorf("expected c.Err() != nil")
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Do("ERR", io.EOF)
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
d.check(".", p, 2, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolClose(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c1 := p.Get()
|
||||||
|
c1.Do("PING")
|
||||||
|
c2 := p.Get()
|
||||||
|
c2.Do("PING")
|
||||||
|
c3 := p.Get()
|
||||||
|
c3.Do("PING")
|
||||||
|
|
||||||
|
c1.Close()
|
||||||
|
if _, err := c1.Do("PING"); err == nil {
|
||||||
|
t.Errorf("expected error after connection closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
c2.Close()
|
||||||
|
c2.Close()
|
||||||
|
|
||||||
|
p.Close()
|
||||||
|
|
||||||
|
d.check("after pool close", p, 3, 1)
|
||||||
|
|
||||||
|
if _, err := c1.Do("PING"); err == nil {
|
||||||
|
t.Errorf("expected error after connection and pool closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
c3.Close()
|
||||||
|
|
||||||
|
d.check("after conn close", p, 3, 0)
|
||||||
|
|
||||||
|
c1 = p.Get()
|
||||||
|
if _, err := c1.Do("PING"); err == nil {
|
||||||
|
t.Errorf("expected error after pool closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolTimeout(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
IdleTimeout: 300 * time.Second,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
redis.SetNowFunc(func() time.Time { return now })
|
||||||
|
defer redis.SetNowFunc(time.Now)
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
d.check("1", p, 1, 1)
|
||||||
|
|
||||||
|
now = now.Add(p.IdleTimeout)
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
d.check("2", p, 2, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolConcurrenSendReceive(t *testing.T) {
|
||||||
|
p := &redis.Pool{
|
||||||
|
Dial: redis.DialDefaultServer,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := c.Receive()
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
c.Send("PING")
|
||||||
|
c.Flush()
|
||||||
|
err := <-done
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Receive() returned error %v", err)
|
||||||
|
}
|
||||||
|
_, err = c.Do("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Do() returned error %v", err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolBorrowCheck(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
TestOnBorrow: func(redis.Conn, time.Time) error { return redis.Error("BLAH") },
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c := p.Get()
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
d.check("1", p, 10, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMaxActive(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
MaxActive: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c1 := p.Get()
|
||||||
|
c1.Do("PING")
|
||||||
|
c2 := p.Get()
|
||||||
|
c2.Do("PING")
|
||||||
|
|
||||||
|
d.check("1", p, 2, 2)
|
||||||
|
|
||||||
|
c3 := p.Get()
|
||||||
|
if _, err := c3.Do("PING"); err != redis.ErrPoolExhausted {
|
||||||
|
t.Errorf("expected pool exhausted")
|
||||||
|
}
|
||||||
|
|
||||||
|
c3.Close()
|
||||||
|
d.check("2", p, 2, 2)
|
||||||
|
c2.Close()
|
||||||
|
d.check("3", p, 2, 2)
|
||||||
|
|
||||||
|
c3 = p.Get()
|
||||||
|
if _, err := c3.Do("PING"); err != nil {
|
||||||
|
t.Errorf("expected good channel, err=%v", err)
|
||||||
|
}
|
||||||
|
c3.Close()
|
||||||
|
|
||||||
|
d.check("4", p, 2, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMonitorCleanup(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
MaxActive: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
c.Send("MONITOR")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
d.check("", p, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolPubSubCleanup(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
MaxActive: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
c.Send("SUBSCRIBE", "x")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Send("PSUBSCRIBE", "x*")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolTransactionCleanup(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 2,
|
||||||
|
MaxActive: 2,
|
||||||
|
Dial: d.dial,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
c.Do("WATCH", "key")
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want := []string{"WATCH", "PING", "UNWATCH"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Do("WATCH", "key")
|
||||||
|
c.Do("UNWATCH")
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want = []string{"WATCH", "UNWATCH", "PING"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Do("WATCH", "key")
|
||||||
|
c.Do("MULTI")
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want = []string{"WATCH", "MULTI", "PING", "DISCARD"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Do("WATCH", "key")
|
||||||
|
c.Do("MULTI")
|
||||||
|
c.Do("DISCARD")
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want = []string{"WATCH", "MULTI", "DISCARD", "PING"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
|
||||||
|
c = p.Get()
|
||||||
|
c.Do("WATCH", "key")
|
||||||
|
c.Do("MULTI")
|
||||||
|
c.Do("EXEC")
|
||||||
|
c.Do("PING")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
want = []string{"WATCH", "MULTI", "EXEC", "PING"}
|
||||||
|
if !reflect.DeepEqual(d.commands, want) {
|
||||||
|
t.Errorf("got commands %v, want %v", d.commands, want)
|
||||||
|
}
|
||||||
|
d.commands = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error {
|
||||||
|
errs := make(chan error, 10)
|
||||||
|
for i := 0; i < cap(errs); i++ {
|
||||||
|
go func() {
|
||||||
|
c := p.Get()
|
||||||
|
_, err := c.Do(cmd, args...)
|
||||||
|
errs <- err
|
||||||
|
c.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for goroutines to block.
|
||||||
|
time.Sleep(time.Second / 4)
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaitPool(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 1,
|
||||||
|
MaxActive: 1,
|
||||||
|
Dial: d.dial,
|
||||||
|
Wait: true,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
errs := startGoroutines(p, "PING")
|
||||||
|
d.check("before close", p, 1, 1)
|
||||||
|
c.Close()
|
||||||
|
timeout := time.After(2 * time.Second)
|
||||||
|
for i := 0; i < cap(errs); i++ {
|
||||||
|
select {
|
||||||
|
case err := <-errs:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("timeout waiting for blocked goroutine %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.check("done", p, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaitPoolClose(t *testing.T) {
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 1,
|
||||||
|
MaxActive: 1,
|
||||||
|
Dial: d.dial,
|
||||||
|
Wait: true,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errs := startGoroutines(p, "PING")
|
||||||
|
d.check("before close", p, 1, 1)
|
||||||
|
p.Close()
|
||||||
|
timeout := time.After(2 * time.Second)
|
||||||
|
for i := 0; i < cap(errs); i++ {
|
||||||
|
select {
|
||||||
|
case err := <-errs:
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
t.Fatal("blocked goroutine did not get error")
|
||||||
|
case redis.ErrPoolExhausted:
|
||||||
|
t.Fatal("blocked goroutine got pool exhausted error")
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatal("timeout waiting for blocked goroutine")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
d.check("done", p, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaitPoolCommandError(t *testing.T) {
|
||||||
|
testErr := errors.New("test")
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 1,
|
||||||
|
MaxActive: 1,
|
||||||
|
Dial: d.dial,
|
||||||
|
Wait: true,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
errs := startGoroutines(p, "ERR", testErr)
|
||||||
|
d.check("before close", p, 1, 1)
|
||||||
|
c.Close()
|
||||||
|
timeout := time.After(2 * time.Second)
|
||||||
|
for i := 0; i < cap(errs); i++ {
|
||||||
|
select {
|
||||||
|
case err := <-errs:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("timeout waiting for blocked goroutine %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.check("done", p, cap(errs), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaitPoolDialError(t *testing.T) {
|
||||||
|
testErr := errors.New("test")
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: 1,
|
||||||
|
MaxActive: 1,
|
||||||
|
Dial: d.dial,
|
||||||
|
Wait: true,
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
c := p.Get()
|
||||||
|
errs := startGoroutines(p, "ERR", testErr)
|
||||||
|
d.check("before close", p, 1, 1)
|
||||||
|
|
||||||
|
d.dialErr = errors.New("dial")
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
nilCount := 0
|
||||||
|
errCount := 0
|
||||||
|
timeout := time.After(2 * time.Second)
|
||||||
|
for i := 0; i < cap(errs); i++ {
|
||||||
|
select {
|
||||||
|
case err := <-errs:
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
nilCount++
|
||||||
|
case d.dialErr:
|
||||||
|
errCount++
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected dial error or nil, got %v", err)
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("timeout waiting for blocked goroutine %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nilCount != 1 {
|
||||||
|
t.Errorf("expected one nil error, got %d", nilCount)
|
||||||
|
}
|
||||||
|
if errCount != cap(errs)-1 {
|
||||||
|
t.Errorf("expected %d dial errors, got %d", cap(errs)-1, errCount)
|
||||||
|
}
|
||||||
|
d.check("done", p, cap(errs), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borrowing requires us to iterate over the idle connections, unlock the pool,
|
||||||
|
// and perform a blocking operation to check the connection still works. If
|
||||||
|
// TestOnBorrow fails, we must reacquire the lock and continue iteration. This
|
||||||
|
// test ensures that iteration will work correctly if multiple threads are
|
||||||
|
// iterating simultaneously.
|
||||||
|
func TestLocking_TestOnBorrowFails_PoolDoesntCrash(t *testing.T) {
|
||||||
|
const count = 100
|
||||||
|
|
||||||
|
// First we'll Create a pool where the pilfering of idle connections fails.
|
||||||
|
d := poolDialer{t: t}
|
||||||
|
p := &redis.Pool{
|
||||||
|
MaxIdle: count,
|
||||||
|
MaxActive: count,
|
||||||
|
Dial: d.dial,
|
||||||
|
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
return errors.New("No way back into the real world.")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// Fill the pool with idle connections.
|
||||||
|
conns := make([]redis.Conn, count)
|
||||||
|
for i := range conns {
|
||||||
|
conns[i] = p.Get()
|
||||||
|
}
|
||||||
|
for i := range conns {
|
||||||
|
conns[i].Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn a bunch of goroutines to thrash the pool.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
go func() {
|
||||||
|
c := p.Get()
|
||||||
|
if c.Err() != nil {
|
||||||
|
t.Errorf("pool get failed: %v", c.Err())
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
if d.dialed != count*2 {
|
||||||
|
t.Errorf("Expected %d dials, got %d", count*2, d.dialed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPoolGet(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2}
|
||||||
|
c := p.Get()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
defer p.Close()
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c = p.Get()
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPoolGetErr(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2}
|
||||||
|
c := p.Get()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
defer p.Close()
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c = p.Get()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPoolGetPing(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2}
|
||||||
|
c := p.Get()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
defer p.Close()
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c = p.Get()
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
31
vendor/github.com/garyburd/redigo/redis/pre_go17.go
generated
vendored
Normal file
31
vendor/github.com/garyburd/redigo/redis/pre_go17.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// +build !go1.7
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import "crypto/tls"
|
||||||
|
|
||||||
|
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
|
||||||
|
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
|
||||||
|
if cfg == nil {
|
||||||
|
return &tls.Config{InsecureSkipVerify: skipVerify}
|
||||||
|
}
|
||||||
|
return &tls.Config{
|
||||||
|
Rand: cfg.Rand,
|
||||||
|
Time: cfg.Time,
|
||||||
|
Certificates: cfg.Certificates,
|
||||||
|
NameToCertificate: cfg.NameToCertificate,
|
||||||
|
GetCertificate: cfg.GetCertificate,
|
||||||
|
RootCAs: cfg.RootCAs,
|
||||||
|
NextProtos: cfg.NextProtos,
|
||||||
|
ServerName: cfg.ServerName,
|
||||||
|
ClientAuth: cfg.ClientAuth,
|
||||||
|
ClientCAs: cfg.ClientCAs,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
|
CipherSuites: cfg.CipherSuites,
|
||||||
|
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||||
|
ClientSessionCache: cfg.ClientSessionCache,
|
||||||
|
MinVersion: cfg.MinVersion,
|
||||||
|
MaxVersion: cfg.MaxVersion,
|
||||||
|
CurvePreferences: cfg.CurvePreferences,
|
||||||
|
}
|
||||||
|
}
|
144
vendor/github.com/garyburd/redigo/redis/pubsub.go
generated
vendored
Normal file
144
vendor/github.com/garyburd/redigo/redis/pubsub.go
generated
vendored
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Subscription represents a subscribe or unsubscribe notification.
|
||||||
|
type Subscription struct {
|
||||||
|
|
||||||
|
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
|
||||||
|
Kind string
|
||||||
|
|
||||||
|
// The channel that was changed.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The current number of subscriptions for connection.
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents a message notification.
|
||||||
|
type Message struct {
|
||||||
|
|
||||||
|
// The originating channel.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The message data.
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PMessage represents a pmessage notification.
|
||||||
|
type PMessage struct {
|
||||||
|
|
||||||
|
// The matched pattern.
|
||||||
|
Pattern string
|
||||||
|
|
||||||
|
// The originating channel.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The message data.
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pong represents a pubsub pong notification.
|
||||||
|
type Pong struct {
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubSubConn wraps a Conn with convenience methods for subscribers.
|
||||||
|
type PubSubConn struct {
|
||||||
|
Conn Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c PubSubConn) Close() error {
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribes the connection to the specified channels.
|
||||||
|
func (c PubSubConn) Subscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("SUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSubscribe subscribes the connection to the given patterns.
|
||||||
|
func (c PubSubConn) PSubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("PSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe unsubscribes the connection from the given channels, or from all
|
||||||
|
// of them if none is given.
|
||||||
|
func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("UNSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUnsubscribe unsubscribes the connection from the given patterns, or from all
|
||||||
|
// of them if none is given.
|
||||||
|
func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("PUNSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping sends a PING to the server with the specified data.
|
||||||
|
func (c PubSubConn) Ping(data string) error {
|
||||||
|
c.Conn.Send("PING", data)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive returns a pushed message as a Subscription, Message, PMessage, Pong
|
||||||
|
// or error. The return value is intended to be used directly in a type switch
|
||||||
|
// as illustrated in the PubSubConn example.
|
||||||
|
func (c PubSubConn) Receive() interface{} {
|
||||||
|
reply, err := Values(c.Conn.Receive())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kind string
|
||||||
|
reply, err = Scan(reply, &kind)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "message":
|
||||||
|
var m Message
|
||||||
|
if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
case "pmessage":
|
||||||
|
var pm PMessage
|
||||||
|
if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pm
|
||||||
|
case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
|
||||||
|
s := Subscription{Kind: kind}
|
||||||
|
if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
case "pong":
|
||||||
|
var p Pong
|
||||||
|
if _, err := Scan(reply, &p.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return errors.New("redigo: unknown pubsub notification")
|
||||||
|
}
|
148
vendor/github.com/garyburd/redigo/redis/pubsub_test.go
generated
vendored
Normal file
148
vendor/github.com/garyburd/redigo/redis/pubsub_test.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
func publish(channel, value interface{}) {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
c.Do("PUBLISH", channel, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applications can receive pushed messages from one goroutine and manage subscriptions from another goroutine.
|
||||||
|
func ExamplePubSubConn() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
psc := redis.PubSubConn{Conn: c}
|
||||||
|
|
||||||
|
// This goroutine receives and prints pushed notifications from the server.
|
||||||
|
// The goroutine exits when the connection is unsubscribed from all
|
||||||
|
// channels or there is an error.
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for {
|
||||||
|
switch n := psc.Receive().(type) {
|
||||||
|
case redis.Message:
|
||||||
|
fmt.Printf("Message: %s %s\n", n.Channel, n.Data)
|
||||||
|
case redis.PMessage:
|
||||||
|
fmt.Printf("PMessage: %s %s %s\n", n.Pattern, n.Channel, n.Data)
|
||||||
|
case redis.Subscription:
|
||||||
|
fmt.Printf("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count)
|
||||||
|
if n.Count == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case error:
|
||||||
|
fmt.Printf("error: %v\n", n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// This goroutine manages subscriptions for the connection.
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
psc.Subscribe("example")
|
||||||
|
psc.PSubscribe("p*")
|
||||||
|
|
||||||
|
// The following function calls publish a message using another
|
||||||
|
// connection to the Redis server.
|
||||||
|
publish("example", "hello")
|
||||||
|
publish("example", "world")
|
||||||
|
publish("pexample", "foo")
|
||||||
|
publish("pexample", "bar")
|
||||||
|
|
||||||
|
// Unsubscribe from all connections. This will cause the receiving
|
||||||
|
// goroutine to exit.
|
||||||
|
psc.Unsubscribe()
|
||||||
|
psc.PUnsubscribe()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Subscription: subscribe example 1
|
||||||
|
// Subscription: psubscribe p* 2
|
||||||
|
// Message: example hello
|
||||||
|
// Message: example world
|
||||||
|
// PMessage: p* pexample foo
|
||||||
|
// PMessage: p* pexample bar
|
||||||
|
// Subscription: unsubscribe example 1
|
||||||
|
// Subscription: punsubscribe p* 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectPushed(t *testing.T, c redis.PubSubConn, message string, expected interface{}) {
|
||||||
|
actual := c.Receive()
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("%s = %v, want %v", message, actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushed(t *testing.T) {
|
||||||
|
pc, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer pc.Close()
|
||||||
|
|
||||||
|
sc, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer sc.Close()
|
||||||
|
|
||||||
|
c := redis.PubSubConn{Conn: sc}
|
||||||
|
|
||||||
|
c.Subscribe("c1")
|
||||||
|
expectPushed(t, c, "Subscribe(c1)", redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1})
|
||||||
|
c.Subscribe("c2")
|
||||||
|
expectPushed(t, c, "Subscribe(c2)", redis.Subscription{Kind: "subscribe", Channel: "c2", Count: 2})
|
||||||
|
c.PSubscribe("p1")
|
||||||
|
expectPushed(t, c, "PSubscribe(p1)", redis.Subscription{Kind: "psubscribe", Channel: "p1", Count: 3})
|
||||||
|
c.PSubscribe("p2")
|
||||||
|
expectPushed(t, c, "PSubscribe(p2)", redis.Subscription{Kind: "psubscribe", Channel: "p2", Count: 4})
|
||||||
|
c.PUnsubscribe()
|
||||||
|
expectPushed(t, c, "Punsubscribe(p1)", redis.Subscription{Kind: "punsubscribe", Channel: "p1", Count: 3})
|
||||||
|
expectPushed(t, c, "Punsubscribe()", redis.Subscription{Kind: "punsubscribe", Channel: "p2", Count: 2})
|
||||||
|
|
||||||
|
pc.Do("PUBLISH", "c1", "hello")
|
||||||
|
expectPushed(t, c, "PUBLISH c1 hello", redis.Message{Channel: "c1", Data: []byte("hello")})
|
||||||
|
|
||||||
|
c.Ping("hello")
|
||||||
|
expectPushed(t, c, `Ping("hello")`, redis.Pong{Data: "hello"})
|
||||||
|
|
||||||
|
c.Conn.Send("PING")
|
||||||
|
c.Conn.Flush()
|
||||||
|
expectPushed(t, c, `Send("PING")`, redis.Pong{})
|
||||||
|
}
|
41
vendor/github.com/garyburd/redigo/redis/redis.go
generated
vendored
Normal file
41
vendor/github.com/garyburd/redigo/redis/redis.go
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
// Error represents an error returned in a command reply.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
func (err Error) Error() string { return string(err) }
|
||||||
|
|
||||||
|
// Conn represents a connection to a Redis server.
|
||||||
|
type Conn interface {
|
||||||
|
// Close closes the connection.
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// Err returns a non-nil value when the connection is not usable.
|
||||||
|
Err() error
|
||||||
|
|
||||||
|
// Do sends a command to the server and returns the received reply.
|
||||||
|
Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||||
|
|
||||||
|
// Send writes the command to the client's output buffer.
|
||||||
|
Send(commandName string, args ...interface{}) error
|
||||||
|
|
||||||
|
// Flush flushes the output buffer to the Redis server.
|
||||||
|
Flush() error
|
||||||
|
|
||||||
|
// Receive receives a single reply from the Redis server
|
||||||
|
Receive() (reply interface{}, err error)
|
||||||
|
}
|
425
vendor/github.com/garyburd/redigo/redis/reply.go
generated
vendored
Normal file
425
vendor/github.com/garyburd/redigo/redis/reply.go
generated
vendored
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNil indicates that a reply value is nil.
|
||||||
|
var ErrNil = errors.New("redigo: nil returned")
|
||||||
|
|
||||||
|
// Int is a helper that converts a command reply to an integer. If err is not
|
||||||
|
// equal to nil, then Int returns 0, err. Otherwise, Int converts the
|
||||||
|
// reply to an int as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer int(reply), nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Int(reply interface{}, err error) (int, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
x := int(reply)
|
||||||
|
if int64(x) != reply {
|
||||||
|
return 0, strconv.ErrRange
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseInt(string(reply), 10, 0)
|
||||||
|
return int(n), err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||||
|
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
|
||||||
|
// reply to an int64 as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer reply, nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Int64(reply interface{}, err error) (int64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
return reply, nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseInt(string(reply), 10, 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNegativeInt = errors.New("redigo: unexpected value for Uint64")
|
||||||
|
|
||||||
|
// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||||
|
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
|
||||||
|
// reply to an int64 as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer reply, nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Uint64(reply interface{}, err error) (uint64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
if reply < 0 {
|
||||||
|
return 0, errNegativeInt
|
||||||
|
}
|
||||||
|
return uint64(reply), nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseUint(string(reply), 10, 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 is a helper that converts a command reply to 64 bit float. If err is
|
||||||
|
// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
|
||||||
|
// the reply to an int as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Float64(reply interface{}, err error) (float64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseFloat(string(reply), 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is a helper that converts a command reply to a string. If err is not
|
||||||
|
// equal to nil, then String returns "", err. Otherwise String converts the
|
||||||
|
// reply to a string as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string string(reply), nil
|
||||||
|
// simple string reply, nil
|
||||||
|
// nil "", ErrNil
|
||||||
|
// other "", error
|
||||||
|
func String(reply interface{}, err error) (string, error) {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
return string(reply), nil
|
||||||
|
case string:
|
||||||
|
return reply, nil
|
||||||
|
case nil:
|
||||||
|
return "", ErrNil
|
||||||
|
case Error:
|
||||||
|
return "", reply
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes is a helper that converts a command reply to a slice of bytes. If err
|
||||||
|
// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
|
||||||
|
// the reply to a slice of bytes as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string reply, nil
|
||||||
|
// simple string []byte(reply), nil
|
||||||
|
// nil nil, ErrNil
|
||||||
|
// other nil, error
|
||||||
|
func Bytes(reply interface{}, err error) ([]byte, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
return reply, nil
|
||||||
|
case string:
|
||||||
|
return []byte(reply), nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool is a helper that converts a command reply to a boolean. If err is not
|
||||||
|
// equal to nil, then Bool returns false, err. Otherwise Bool converts the
|
||||||
|
// reply to boolean as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer value != 0, nil
|
||||||
|
// bulk string strconv.ParseBool(reply)
|
||||||
|
// nil false, ErrNil
|
||||||
|
// other false, error
|
||||||
|
func Bool(reply interface{}, err error) (bool, error) {
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
return reply != 0, nil
|
||||||
|
case []byte:
|
||||||
|
return strconv.ParseBool(string(reply))
|
||||||
|
case nil:
|
||||||
|
return false, ErrNil
|
||||||
|
case Error:
|
||||||
|
return false, reply
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiBulk is a helper that converts an array command reply to a []interface{}.
|
||||||
|
//
|
||||||
|
// Deprecated: Use Values instead.
|
||||||
|
func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
|
||||||
|
|
||||||
|
// Values is a helper that converts an array command reply to a []interface{}.
|
||||||
|
// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
|
||||||
|
// converts the reply as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// array reply, nil
|
||||||
|
// nil nil, ErrNil
|
||||||
|
// other nil, error
|
||||||
|
func Values(reply interface{}, err error) ([]interface{}, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
return reply, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings is a helper that converts an array command reply to a []string. If
|
||||||
|
// err is not equal to nil, then Strings returns nil, err. Nil array items are
|
||||||
|
// converted to "" in the output slice. Strings returns an error if an array
|
||||||
|
// item is not a bulk string or nil.
|
||||||
|
func Strings(reply interface{}, err error) ([]string, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
result := make([]string, len(reply))
|
||||||
|
for i := range reply {
|
||||||
|
if reply[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := reply[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for Strings, got type %T", reply[i])
|
||||||
|
}
|
||||||
|
result[i] = string(p)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Strings, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSlices is a helper that converts an array command reply to a [][]byte.
|
||||||
|
// If err is not equal to nil, then ByteSlices returns nil, err. Nil array
|
||||||
|
// items are stay nil. ByteSlices returns an error if an array item is not a
|
||||||
|
// bulk string or nil.
|
||||||
|
func ByteSlices(reply interface{}, err error) ([][]byte, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
result := make([][]byte, len(reply))
|
||||||
|
for i := range reply {
|
||||||
|
if reply[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := reply[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", reply[i])
|
||||||
|
}
|
||||||
|
result[i] = p
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for ByteSlices, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ints is a helper that converts an array command reply to a []int. If
|
||||||
|
// err is not equal to nil, then Ints returns nil, err.
|
||||||
|
func Ints(reply interface{}, err error) ([]int, error) {
|
||||||
|
var ints []int
|
||||||
|
values, err := Values(reply, err)
|
||||||
|
if err != nil {
|
||||||
|
return ints, err
|
||||||
|
}
|
||||||
|
if err := ScanSlice(values, &ints); err != nil {
|
||||||
|
return ints, err
|
||||||
|
}
|
||||||
|
return ints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMap is a helper that converts an array of strings (alternating key, value)
|
||||||
|
// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format.
|
||||||
|
// Requires an even number of values in result.
|
||||||
|
func StringMap(result interface{}, err error) (map[string]string, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("redigo: StringMap expects even number of values result")
|
||||||
|
}
|
||||||
|
m := make(map[string]string, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, okKey := values[i].([]byte)
|
||||||
|
value, okValue := values[i+1].([]byte)
|
||||||
|
if !okKey || !okValue {
|
||||||
|
return nil, errors.New("redigo: ScanMap key not a bulk string value")
|
||||||
|
}
|
||||||
|
m[string(key)] = string(value)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntMap is a helper that converts an array of strings (alternating key, value)
|
||||||
|
// into a map[string]int. The HGETALL commands return replies in this format.
|
||||||
|
// Requires an even number of values in result.
|
||||||
|
func IntMap(result interface{}, err error) (map[string]int, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("redigo: IntMap expects even number of values result")
|
||||||
|
}
|
||||||
|
m := make(map[string]int, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("redigo: ScanMap key not a bulk string value")
|
||||||
|
}
|
||||||
|
value, err := Int(values[i+1], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m[string(key)] = value
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Map is a helper that converts an array of strings (alternating key, value)
|
||||||
|
// into a map[string]int64. The HGETALL commands return replies in this format.
|
||||||
|
// Requires an even number of values in result.
|
||||||
|
func Int64Map(result interface{}, err error) (map[string]int64, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("redigo: Int64Map expects even number of values result")
|
||||||
|
}
|
||||||
|
m := make(map[string]int64, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("redigo: ScanMap key not a bulk string value")
|
||||||
|
}
|
||||||
|
value, err := Int64(values[i+1], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m[string(key)] = value
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions is a helper that converts an array of positions (lat, long)
|
||||||
|
// into a [][2]float64. The GEOPOS command returns replies in this format.
|
||||||
|
func Positions(result interface{}, err error) ([]*[2]float64, error) {
|
||||||
|
values, err := Values(result, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
positions := make([]*[2]float64, len(values))
|
||||||
|
for i := range values {
|
||||||
|
if values[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := values[i].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i])
|
||||||
|
}
|
||||||
|
if len(p) != 2 {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p))
|
||||||
|
}
|
||||||
|
lat, err := Float64(p[0], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
long, err := Float64(p[1], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
positions[i] = &[2]float64{lat, long}
|
||||||
|
}
|
||||||
|
return positions, nil
|
||||||
|
}
|
184
vendor/github.com/garyburd/redigo/redis/reply_test.go
generated
vendored
Normal file
184
vendor/github.com/garyburd/redigo/redis/reply_test.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type valueError struct {
|
||||||
|
v interface{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func ve(v interface{}, err error) valueError {
|
||||||
|
return valueError{v, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
var replyTests = []struct {
|
||||||
|
name interface{}
|
||||||
|
actual valueError
|
||||||
|
expected valueError
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ints([v1, v2])",
|
||||||
|
ve(redis.Ints([]interface{}{[]byte("4"), []byte("5")}, nil)),
|
||||||
|
ve([]int{4, 5}, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ints(nil)",
|
||||||
|
ve(redis.Ints(nil, nil)),
|
||||||
|
ve([]int(nil), redis.ErrNil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"strings([v1, v2])",
|
||||||
|
ve(redis.Strings([]interface{}{[]byte("v1"), []byte("v2")}, nil)),
|
||||||
|
ve([]string{"v1", "v2"}, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"strings(nil)",
|
||||||
|
ve(redis.Strings(nil, nil)),
|
||||||
|
ve([]string(nil), redis.ErrNil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"byteslices([v1, v2])",
|
||||||
|
ve(redis.ByteSlices([]interface{}{[]byte("v1"), []byte("v2")}, nil)),
|
||||||
|
ve([][]byte{[]byte("v1"), []byte("v2")}, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"byteslices(nil)",
|
||||||
|
ve(redis.ByteSlices(nil, nil)),
|
||||||
|
ve([][]byte(nil), redis.ErrNil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"values([v1, v2])",
|
||||||
|
ve(redis.Values([]interface{}{[]byte("v1"), []byte("v2")}, nil)),
|
||||||
|
ve([]interface{}{[]byte("v1"), []byte("v2")}, nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"values(nil)",
|
||||||
|
ve(redis.Values(nil, nil)),
|
||||||
|
ve([]interface{}(nil), redis.ErrNil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"float64(1.0)",
|
||||||
|
ve(redis.Float64([]byte("1.0"), nil)),
|
||||||
|
ve(float64(1.0), nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"float64(nil)",
|
||||||
|
ve(redis.Float64(nil, nil)),
|
||||||
|
ve(float64(0.0), redis.ErrNil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uint64(1)",
|
||||||
|
ve(redis.Uint64(int64(1), nil)),
|
||||||
|
ve(uint64(1), nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uint64(-1)",
|
||||||
|
ve(redis.Uint64(int64(-1), nil)),
|
||||||
|
ve(uint64(0), redis.ErrNegativeInt),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions([[1, 2], nil, [3, 4]])",
|
||||||
|
ve(redis.Positions([]interface{}{[]interface{}{[]byte("1"), []byte("2")}, nil, []interface{}{[]byte("3"), []byte("4")}}, nil)),
|
||||||
|
ve([]*[2]float64{{1.0, 2.0}, nil, {3.0, 4.0}}, nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReply(t *testing.T) {
|
||||||
|
for _, rt := range replyTests {
|
||||||
|
if rt.actual.err != rt.expected.err {
|
||||||
|
t.Errorf("%s returned err %v, want %v", rt.name, rt.actual.err, rt.expected.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(rt.actual.v, rt.expected.v) {
|
||||||
|
t.Errorf("%s=%+v, want %+v", rt.name, rt.actual.v, rt.expected.v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dial wraps DialDefaultServer() with a more suitable function name for examples.
|
||||||
|
func dial() (redis.Conn, error) {
|
||||||
|
return redis.DialDefaultServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleBool() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Do("SET", "foo", 1)
|
||||||
|
exists, _ := redis.Bool(c.Do("EXISTS", "foo"))
|
||||||
|
fmt.Printf("%#v\n", exists)
|
||||||
|
// Output:
|
||||||
|
// true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleInt() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Do("SET", "k1", 1)
|
||||||
|
n, _ := redis.Int(c.Do("GET", "k1"))
|
||||||
|
fmt.Printf("%#v\n", n)
|
||||||
|
n, _ = redis.Int(c.Do("INCR", "k1"))
|
||||||
|
fmt.Printf("%#v\n", n)
|
||||||
|
// Output:
|
||||||
|
// 1
|
||||||
|
// 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleInts() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Do("SADD", "set_with_integers", 4, 5, 6)
|
||||||
|
ints, _ := redis.Ints(c.Do("SMEMBERS", "set_with_integers"))
|
||||||
|
fmt.Printf("%#v\n", ints)
|
||||||
|
// Output:
|
||||||
|
// []int{4, 5, 6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleString() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Do("SET", "hello", "world")
|
||||||
|
s, err := redis.String(c.Do("GET", "hello"))
|
||||||
|
fmt.Printf("%#v\n", s)
|
||||||
|
// Output:
|
||||||
|
// "world"
|
||||||
|
}
|
559
vendor/github.com/garyburd/redigo/redis/scan.go
generated
vendored
Normal file
559
vendor/github.com/garyburd/redigo/redis/scan.go
generated
vendored
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureLen(d reflect.Value, n int) {
|
||||||
|
if n > d.Cap() {
|
||||||
|
d.Set(reflect.MakeSlice(d.Type(), n, n))
|
||||||
|
} else {
|
||||||
|
d.SetLen(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cannotConvert(d reflect.Value, s interface{}) error {
|
||||||
|
var sname string
|
||||||
|
switch s.(type) {
|
||||||
|
case string:
|
||||||
|
sname = "Redis simple string"
|
||||||
|
case Error:
|
||||||
|
sname = "Redis error"
|
||||||
|
case int64:
|
||||||
|
sname = "Redis integer"
|
||||||
|
case []byte:
|
||||||
|
sname = "Redis bulk string"
|
||||||
|
case []interface{}:
|
||||||
|
sname = "Redis array"
|
||||||
|
default:
|
||||||
|
sname = reflect.TypeOf(s).String()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot convert from %s to %s", sname, d.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignBulkString(d reflect.Value, s []byte) (err error) {
|
||||||
|
switch d.Type().Kind() {
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
var x float64
|
||||||
|
x, err = strconv.ParseFloat(string(s), d.Type().Bits())
|
||||||
|
d.SetFloat(x)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
var x int64
|
||||||
|
x, err = strconv.ParseInt(string(s), 10, d.Type().Bits())
|
||||||
|
d.SetInt(x)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
var x uint64
|
||||||
|
x, err = strconv.ParseUint(string(s), 10, d.Type().Bits())
|
||||||
|
d.SetUint(x)
|
||||||
|
case reflect.Bool:
|
||||||
|
var x bool
|
||||||
|
x, err = strconv.ParseBool(string(s))
|
||||||
|
d.SetBool(x)
|
||||||
|
case reflect.String:
|
||||||
|
d.SetString(string(s))
|
||||||
|
case reflect.Slice:
|
||||||
|
if d.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
d.SetBytes(s)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignInt(d reflect.Value, s int64) (err error) {
|
||||||
|
switch d.Type().Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
d.SetInt(s)
|
||||||
|
if d.Int() != s {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
d.SetInt(0)
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if s < 0 {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
} else {
|
||||||
|
x := uint64(s)
|
||||||
|
d.SetUint(x)
|
||||||
|
if d.Uint() != x {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
d.SetUint(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
d.SetBool(s != 0)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignValue(d reflect.Value, s interface{}) (err error) {
|
||||||
|
switch s := s.(type) {
|
||||||
|
case []byte:
|
||||||
|
err = convertAssignBulkString(d, s)
|
||||||
|
case int64:
|
||||||
|
err = convertAssignInt(d, s)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignArray(d reflect.Value, s []interface{}) error {
|
||||||
|
if d.Type().Kind() != reflect.Slice {
|
||||||
|
return cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
ensureLen(d, len(s))
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if err := convertAssignValue(d.Index(i), s[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssign(d interface{}, s interface{}) (err error) {
|
||||||
|
// Handle the most common destination types using type switches and
|
||||||
|
// fall back to reflection for all other types.
|
||||||
|
switch s := s.(type) {
|
||||||
|
case nil:
|
||||||
|
// ingore
|
||||||
|
case []byte:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *string:
|
||||||
|
*d = string(s)
|
||||||
|
case *int:
|
||||||
|
*d, err = strconv.Atoi(string(s))
|
||||||
|
case *bool:
|
||||||
|
*d, err = strconv.ParseBool(string(s))
|
||||||
|
case *[]byte:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignBulkString(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *int:
|
||||||
|
x := int(s)
|
||||||
|
if int64(x) != s {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
x = 0
|
||||||
|
}
|
||||||
|
*d = x
|
||||||
|
case *bool:
|
||||||
|
*d = s != 0
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignInt(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *string:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
err = cannotConvert(reflect.ValueOf(d), s)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *[]interface{}:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignArray(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Error:
|
||||||
|
err = s
|
||||||
|
default:
|
||||||
|
err = cannotConvert(reflect.ValueOf(d), s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies from src to the values pointed at by dest.
|
||||||
|
//
|
||||||
|
// The values pointed at by dest must be an integer, float, boolean, string,
|
||||||
|
// []byte, interface{} or slices of these types. Scan uses the standard strconv
|
||||||
|
// package to convert bulk strings to numeric and boolean types.
|
||||||
|
//
|
||||||
|
// If a dest value is nil, then the corresponding src value is skipped.
|
||||||
|
//
|
||||||
|
// If a src element is nil, then the corresponding dest value is not modified.
|
||||||
|
//
|
||||||
|
// To enable easy use of Scan in a loop, Scan returns the slice of src
|
||||||
|
// following the copied values.
|
||||||
|
func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
|
||||||
|
if len(src) < len(dest) {
|
||||||
|
return nil, errors.New("redigo.Scan: array short")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
for i, d := range dest {
|
||||||
|
err = convertAssign(d, src[i])
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src[len(dest):], err
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldSpec struct {
|
||||||
|
name string
|
||||||
|
index []int
|
||||||
|
omitEmpty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type structSpec struct {
|
||||||
|
m map[string]*fieldSpec
|
||||||
|
l []*fieldSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
|
||||||
|
return ss.m[string(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
f := t.Field(i)
|
||||||
|
switch {
|
||||||
|
case f.PkgPath != "" && !f.Anonymous:
|
||||||
|
// Ignore unexported fields.
|
||||||
|
case f.Anonymous:
|
||||||
|
// TODO: Handle pointers. Requires change to decoder and
|
||||||
|
// protection against infinite recursion.
|
||||||
|
if f.Type.Kind() == reflect.Struct {
|
||||||
|
compileStructSpec(f.Type, depth, append(index, i), ss)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fs := &fieldSpec{name: f.Name}
|
||||||
|
tag := f.Tag.Get("redis")
|
||||||
|
p := strings.Split(tag, ",")
|
||||||
|
if len(p) > 0 {
|
||||||
|
if p[0] == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(p[0]) > 0 {
|
||||||
|
fs.name = p[0]
|
||||||
|
}
|
||||||
|
for _, s := range p[1:] {
|
||||||
|
switch s {
|
||||||
|
case "omitempty":
|
||||||
|
fs.omitEmpty = true
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d, found := depth[fs.name]
|
||||||
|
if !found {
|
||||||
|
d = 1 << 30
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(index) == d:
|
||||||
|
// At same depth, remove from result.
|
||||||
|
delete(ss.m, fs.name)
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < len(ss.l); i++ {
|
||||||
|
if fs.name != ss.l[i].name {
|
||||||
|
ss.l[j] = ss.l[i]
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.l = ss.l[:j]
|
||||||
|
case len(index) < d:
|
||||||
|
fs.index = make([]int, len(index)+1)
|
||||||
|
copy(fs.index, index)
|
||||||
|
fs.index[len(index)] = i
|
||||||
|
depth[fs.name] = len(index)
|
||||||
|
ss.m[fs.name] = fs
|
||||||
|
ss.l = append(ss.l, fs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
structSpecMutex sync.RWMutex
|
||||||
|
structSpecCache = make(map[reflect.Type]*structSpec)
|
||||||
|
defaultFieldSpec = &fieldSpec{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func structSpecForType(t reflect.Type) *structSpec {
|
||||||
|
|
||||||
|
structSpecMutex.RLock()
|
||||||
|
ss, found := structSpecCache[t]
|
||||||
|
structSpecMutex.RUnlock()
|
||||||
|
if found {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
structSpecMutex.Lock()
|
||||||
|
defer structSpecMutex.Unlock()
|
||||||
|
ss, found = structSpecCache[t]
|
||||||
|
if found {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
ss = &structSpec{m: make(map[string]*fieldSpec)}
|
||||||
|
compileStructSpec(t, make(map[string]int), nil, ss)
|
||||||
|
structSpecCache[t] = ss
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct")
|
||||||
|
|
||||||
|
// ScanStruct scans alternating names and values from src to a struct. The
|
||||||
|
// HGETALL and CONFIG GET commands return replies in this format.
|
||||||
|
//
|
||||||
|
// ScanStruct uses exported field names to match values in the response. Use
|
||||||
|
// 'redis' field tag to override the name:
|
||||||
|
//
|
||||||
|
// Field int `redis:"myName"`
|
||||||
|
//
|
||||||
|
// Fields with the tag redis:"-" are ignored.
|
||||||
|
//
|
||||||
|
// Integer, float, boolean, string and []byte fields are supported. Scan uses the
|
||||||
|
// standard strconv package to convert bulk string values to numeric and
|
||||||
|
// boolean types.
|
||||||
|
//
|
||||||
|
// If a src element is nil, then the corresponding field is not modified.
|
||||||
|
func ScanStruct(src []interface{}, dest interface{}) error {
|
||||||
|
d := reflect.ValueOf(dest)
|
||||||
|
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||||
|
return errScanStructValue
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
if d.Kind() != reflect.Struct {
|
||||||
|
return errScanStructValue
|
||||||
|
}
|
||||||
|
ss := structSpecForType(d.Type())
|
||||||
|
|
||||||
|
if len(src)%2 != 0 {
|
||||||
|
return errors.New("redigo.ScanStruct: number of values not a multiple of 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(src); i += 2 {
|
||||||
|
s := src[i+1]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, ok := src[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i)
|
||||||
|
}
|
||||||
|
fs := ss.fieldSpec(name)
|
||||||
|
if fs == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||||
|
return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScanSlice scans src to the slice pointed to by dest. The elements the dest
|
||||||
|
// slice must be integer, float, boolean, string, struct or pointer to struct
|
||||||
|
// values.
|
||||||
|
//
|
||||||
|
// Struct fields must be integer, float, boolean or string values. All struct
|
||||||
|
// fields are used unless a subset is specified using fieldNames.
|
||||||
|
func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
|
||||||
|
d := reflect.ValueOf(dest)
|
||||||
|
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||||
|
return errScanSliceValue
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
if d.Kind() != reflect.Slice {
|
||||||
|
return errScanSliceValue
|
||||||
|
}
|
||||||
|
|
||||||
|
isPtr := false
|
||||||
|
t := d.Type().Elem()
|
||||||
|
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
|
||||||
|
isPtr = true
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
ensureLen(d, len(src))
|
||||||
|
for i, s := range src {
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.Index(i), s); err != nil {
|
||||||
|
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := structSpecForType(t)
|
||||||
|
fss := ss.l
|
||||||
|
if len(fieldNames) > 0 {
|
||||||
|
fss = make([]*fieldSpec, len(fieldNames))
|
||||||
|
for i, name := range fieldNames {
|
||||||
|
fss[i] = ss.m[name]
|
||||||
|
if fss[i] == nil {
|
||||||
|
return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fss) == 0 {
|
||||||
|
return errors.New("redigo.ScanSlice: no struct fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(src) / len(fss)
|
||||||
|
if n*len(fss) != len(src) {
|
||||||
|
return errors.New("redigo.ScanSlice: length not a multiple of struct field count")
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLen(d, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
d := d.Index(i)
|
||||||
|
if isPtr {
|
||||||
|
if d.IsNil() {
|
||||||
|
d.Set(reflect.New(t))
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
}
|
||||||
|
for j, fs := range fss {
|
||||||
|
s := src[i*len(fss)+j]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||||
|
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args is a helper for constructing command arguments from structured values.
|
||||||
|
type Args []interface{}
|
||||||
|
|
||||||
|
// Add returns the result of appending value to args.
|
||||||
|
func (args Args) Add(value ...interface{}) Args {
|
||||||
|
return append(args, value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlat returns the result of appending the flattened value of v to args.
|
||||||
|
//
|
||||||
|
// Maps are flattened by appending the alternating keys and map values to args.
|
||||||
|
//
|
||||||
|
// Slices are flattened by appending the slice elements to args.
|
||||||
|
//
|
||||||
|
// Structs are flattened by appending the alternating names and values of
|
||||||
|
// exported fields to args. If v is a nil struct pointer, then nothing is
|
||||||
|
// appended. The 'redis' field tag overrides struct field names. See ScanStruct
|
||||||
|
// for more information on the use of the 'redis' field tag.
|
||||||
|
//
|
||||||
|
// Other types are appended to args as is.
|
||||||
|
func (args Args) AddFlat(v interface{}) Args {
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
args = flattenStruct(args, rv)
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
args = append(args, rv.Index(i).Interface())
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
for _, k := range rv.MapKeys() {
|
||||||
|
args = append(args, k.Interface(), rv.MapIndex(k).Interface())
|
||||||
|
}
|
||||||
|
case reflect.Ptr:
|
||||||
|
if rv.Type().Elem().Kind() == reflect.Struct {
|
||||||
|
if !rv.IsNil() {
|
||||||
|
args = flattenStruct(args, rv.Elem())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenStruct(args Args, v reflect.Value) Args {
|
||||||
|
ss := structSpecForType(v.Type())
|
||||||
|
for _, fs := range ss.l {
|
||||||
|
fv := v.FieldByIndex(fs.index)
|
||||||
|
if fs.omitEmpty {
|
||||||
|
var empty = false
|
||||||
|
switch fv.Kind() {
|
||||||
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||||
|
empty = fv.Len() == 0
|
||||||
|
case reflect.Bool:
|
||||||
|
empty = !fv.Bool()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
empty = fv.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
empty = fv.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
empty = fv.Float() == 0
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
empty = fv.IsNil()
|
||||||
|
}
|
||||||
|
if empty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args = append(args, fs.name, fv.Interface())
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
440
vendor/github.com/garyburd/redigo/redis/scan_test.go
generated
vendored
Normal file
440
vendor/github.com/garyburd/redigo/redis/scan_test.go
generated
vendored
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
var scanConversionTests = []struct {
|
||||||
|
src interface{}
|
||||||
|
dest interface{}
|
||||||
|
}{
|
||||||
|
{[]byte("-inf"), math.Inf(-1)},
|
||||||
|
{[]byte("+inf"), math.Inf(1)},
|
||||||
|
{[]byte("0"), float64(0)},
|
||||||
|
{[]byte("3.14159"), float64(3.14159)},
|
||||||
|
{[]byte("3.14"), float32(3.14)},
|
||||||
|
{[]byte("-100"), int(-100)},
|
||||||
|
{[]byte("101"), int(101)},
|
||||||
|
{int64(102), int(102)},
|
||||||
|
{[]byte("103"), uint(103)},
|
||||||
|
{int64(104), uint(104)},
|
||||||
|
{[]byte("105"), int8(105)},
|
||||||
|
{int64(106), int8(106)},
|
||||||
|
{[]byte("107"), uint8(107)},
|
||||||
|
{int64(108), uint8(108)},
|
||||||
|
{[]byte("0"), false},
|
||||||
|
{int64(0), false},
|
||||||
|
{[]byte("f"), false},
|
||||||
|
{[]byte("1"), true},
|
||||||
|
{int64(1), true},
|
||||||
|
{[]byte("t"), true},
|
||||||
|
{"hello", "hello"},
|
||||||
|
{[]byte("hello"), "hello"},
|
||||||
|
{[]byte("world"), []byte("world")},
|
||||||
|
{[]interface{}{[]byte("foo")}, []interface{}{[]byte("foo")}},
|
||||||
|
{[]interface{}{[]byte("foo")}, []string{"foo"}},
|
||||||
|
{[]interface{}{[]byte("hello"), []byte("world")}, []string{"hello", "world"}},
|
||||||
|
{[]interface{}{[]byte("bar")}, [][]byte{[]byte("bar")}},
|
||||||
|
{[]interface{}{[]byte("1")}, []int{1}},
|
||||||
|
{[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}},
|
||||||
|
{[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}},
|
||||||
|
{[]interface{}{[]byte("1")}, []byte{1}},
|
||||||
|
{[]interface{}{[]byte("1")}, []bool{true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanConversion(t *testing.T) {
|
||||||
|
for _, tt := range scanConversionTests {
|
||||||
|
values := []interface{}{tt.src}
|
||||||
|
dest := reflect.New(reflect.TypeOf(tt.dest))
|
||||||
|
values, err := redis.Scan(values, dest.Interface())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Scan(%v) returned error %v", tt, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) {
|
||||||
|
t.Errorf("Scan(%v) returned %v, want %v", tt, dest.Elem().Interface(), tt.dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanConversionErrorTests = []struct {
|
||||||
|
src interface{}
|
||||||
|
dest interface{}
|
||||||
|
}{
|
||||||
|
{[]byte("1234"), byte(0)},
|
||||||
|
{int64(1234), byte(0)},
|
||||||
|
{[]byte("-1"), byte(0)},
|
||||||
|
{int64(-1), byte(0)},
|
||||||
|
{[]byte("junk"), false},
|
||||||
|
{redis.Error("blah"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanConversionError(t *testing.T) {
|
||||||
|
for _, tt := range scanConversionErrorTests {
|
||||||
|
values := []interface{}{tt.src}
|
||||||
|
dest := reflect.New(reflect.TypeOf(tt.dest))
|
||||||
|
values, err := redis.Scan(values, dest.Interface())
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Scan(%v) did not return error", tt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScan() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Send("HMSET", "album:1", "title", "Red", "rating", 5)
|
||||||
|
c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1)
|
||||||
|
c.Send("HMSET", "album:3", "title", "Beat")
|
||||||
|
c.Send("LPUSH", "albums", "1")
|
||||||
|
c.Send("LPUSH", "albums", "2")
|
||||||
|
c.Send("LPUSH", "albums", "3")
|
||||||
|
values, err := redis.Values(c.Do("SORT", "albums",
|
||||||
|
"BY", "album:*->rating",
|
||||||
|
"GET", "album:*->title",
|
||||||
|
"GET", "album:*->rating"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(values) > 0 {
|
||||||
|
var title string
|
||||||
|
rating := -1 // initialize to illegal value to detect nil.
|
||||||
|
values, err = redis.Scan(values, &title, &rating)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rating == -1 {
|
||||||
|
fmt.Println(title, "not-rated")
|
||||||
|
} else {
|
||||||
|
fmt.Println(title, rating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// Beat not-rated
|
||||||
|
// Earthbound 1
|
||||||
|
// Red 5
|
||||||
|
}
|
||||||
|
|
||||||
|
type s0 struct {
|
||||||
|
X int
|
||||||
|
Y int `redis:"y"`
|
||||||
|
Bt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type s1 struct {
|
||||||
|
X int `redis:"-"`
|
||||||
|
I int `redis:"i"`
|
||||||
|
U uint `redis:"u"`
|
||||||
|
S string `redis:"s"`
|
||||||
|
P []byte `redis:"p"`
|
||||||
|
B bool `redis:"b"`
|
||||||
|
Bt bool
|
||||||
|
Bf bool
|
||||||
|
s0
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanStructTests = []struct {
|
||||||
|
title string
|
||||||
|
reply []string
|
||||||
|
value interface{}
|
||||||
|
}{
|
||||||
|
{"basic",
|
||||||
|
[]string{"i", "-1234", "u", "5678", "s", "hello", "p", "world", "b", "t", "Bt", "1", "Bf", "0", "X", "123", "y", "456"},
|
||||||
|
&s1{I: -1234, U: 5678, S: "hello", P: []byte("world"), B: true, Bt: true, Bf: false, s0: s0{X: 123, Y: 456}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanStruct(t *testing.T) {
|
||||||
|
for _, tt := range scanStructTests {
|
||||||
|
|
||||||
|
var reply []interface{}
|
||||||
|
for _, v := range tt.reply {
|
||||||
|
reply = append(reply, []byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
value := reflect.New(reflect.ValueOf(tt.value).Type().Elem())
|
||||||
|
|
||||||
|
if err := redis.ScanStruct(reply, value.Interface()); err != nil {
|
||||||
|
t.Fatalf("ScanStruct(%s) returned error %v", tt.title, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(value.Interface(), tt.value) {
|
||||||
|
t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadScanStructArgs(t *testing.T) {
|
||||||
|
x := []interface{}{"A", "b"}
|
||||||
|
test := func(v interface{}) {
|
||||||
|
if err := redis.ScanStruct(x, v); err == nil {
|
||||||
|
t.Errorf("Expect error for ScanStruct(%T, %T)", x, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test(nil)
|
||||||
|
|
||||||
|
var v0 *struct{}
|
||||||
|
test(v0)
|
||||||
|
|
||||||
|
var v1 int
|
||||||
|
test(&v1)
|
||||||
|
|
||||||
|
x = x[:1]
|
||||||
|
v2 := struct{ A string }{}
|
||||||
|
test(&v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanSliceTests = []struct {
|
||||||
|
src []interface{}
|
||||||
|
fieldNames []string
|
||||||
|
ok bool
|
||||||
|
dest interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("1"), nil, []byte("-1")},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
[]int{1, 0, -1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("1"), nil, []byte("2")},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
[]uint{1, 0, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("-1")},
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
[]uint{1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("hello"), nil, []byte("world")},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
[][]byte{[]byte("hello"), nil, []byte("world")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("hello"), nil, []byte("world")},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
[]string{"hello", "", "world"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
[]struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("a1"), []byte("b1")},
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
[]struct{ A, B, C string }{{"a1", "b1", ""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
[]*struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
||||||
|
[]string{"A", "B"},
|
||||||
|
true,
|
||||||
|
[]struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
[]struct{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanSlice(t *testing.T) {
|
||||||
|
for _, tt := range scanSliceTests {
|
||||||
|
|
||||||
|
typ := reflect.ValueOf(tt.dest).Type()
|
||||||
|
dest := reflect.New(typ)
|
||||||
|
|
||||||
|
err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...)
|
||||||
|
if tt.ok != (err == nil) {
|
||||||
|
t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) {
|
||||||
|
t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScanSlice() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
c.Send("HMSET", "album:1", "title", "Red", "rating", 5)
|
||||||
|
c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1)
|
||||||
|
c.Send("HMSET", "album:3", "title", "Beat", "rating", 4)
|
||||||
|
c.Send("LPUSH", "albums", "1")
|
||||||
|
c.Send("LPUSH", "albums", "2")
|
||||||
|
c.Send("LPUSH", "albums", "3")
|
||||||
|
values, err := redis.Values(c.Do("SORT", "albums",
|
||||||
|
"BY", "album:*->rating",
|
||||||
|
"GET", "album:*->title",
|
||||||
|
"GET", "album:*->rating"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var albums []struct {
|
||||||
|
Title string
|
||||||
|
Rating int
|
||||||
|
}
|
||||||
|
if err := redis.ScanSlice(values, &albums); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", albums)
|
||||||
|
// Output:
|
||||||
|
// [{Earthbound 1} {Beat 4} {Red 5}]
|
||||||
|
}
|
||||||
|
|
||||||
|
var argsTests = []struct {
|
||||||
|
title string
|
||||||
|
actual redis.Args
|
||||||
|
expected redis.Args
|
||||||
|
}{
|
||||||
|
{"struct ptr",
|
||||||
|
redis.Args{}.AddFlat(&struct {
|
||||||
|
I int `redis:"i"`
|
||||||
|
U uint `redis:"u"`
|
||||||
|
S string `redis:"s"`
|
||||||
|
P []byte `redis:"p"`
|
||||||
|
M map[string]string `redis:"m"`
|
||||||
|
Bt bool
|
||||||
|
Bf bool
|
||||||
|
}{
|
||||||
|
-1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false,
|
||||||
|
}),
|
||||||
|
redis.Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false},
|
||||||
|
},
|
||||||
|
{"struct",
|
||||||
|
redis.Args{}.AddFlat(struct{ I int }{123}),
|
||||||
|
redis.Args{"I", 123},
|
||||||
|
},
|
||||||
|
{"slice",
|
||||||
|
redis.Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2),
|
||||||
|
redis.Args{1, "a", "b", "c", 2},
|
||||||
|
},
|
||||||
|
{"struct omitempty",
|
||||||
|
redis.Args{}.AddFlat(&struct {
|
||||||
|
I int `redis:"i,omitempty"`
|
||||||
|
U uint `redis:"u,omitempty"`
|
||||||
|
S string `redis:"s,omitempty"`
|
||||||
|
P []byte `redis:"p,omitempty"`
|
||||||
|
M map[string]string `redis:"m,omitempty"`
|
||||||
|
Bt bool `redis:"Bt,omitempty"`
|
||||||
|
Bf bool `redis:"Bf,omitempty"`
|
||||||
|
}{
|
||||||
|
0, 0, "", []byte{}, map[string]string{}, true, false,
|
||||||
|
}),
|
||||||
|
redis.Args{"Bt", true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgs(t *testing.T) {
|
||||||
|
for _, tt := range argsTests {
|
||||||
|
if !reflect.DeepEqual(tt.actual, tt.expected) {
|
||||||
|
t.Fatalf("%s is %v, want %v", tt.title, tt.actual, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleArgs() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
var p1, p2 struct {
|
||||||
|
Title string `redis:"title"`
|
||||||
|
Author string `redis:"author"`
|
||||||
|
Body string `redis:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p1.Title = "Example"
|
||||||
|
p1.Author = "Gary"
|
||||||
|
p1.Body = "Hello"
|
||||||
|
|
||||||
|
if _, err := c.Do("HMSET", redis.Args{}.Add("id1").AddFlat(&p1)...); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]string{
|
||||||
|
"title": "Example2",
|
||||||
|
"author": "Steve",
|
||||||
|
"body": "Map",
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.Do("HMSET", redis.Args{}.Add("id2").AddFlat(m)...); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range []string{"id1", "id2"} {
|
||||||
|
|
||||||
|
v, err := redis.Values(c.Do("HGETALL", id))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := redis.ScanStruct(v, &p2); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v\n", p2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// {Title:Example Author:Gary Body:Hello}
|
||||||
|
// {Title:Example2 Author:Steve Body:Map}
|
||||||
|
}
|
86
vendor/github.com/garyburd/redigo/redis/script.go
generated
vendored
Normal file
86
vendor/github.com/garyburd/redigo/redis/script.go
generated
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Script encapsulates the source, hash and key count for a Lua script. See
|
||||||
|
// http://redis.io/commands/eval for information on scripts in Redis.
|
||||||
|
type Script struct {
|
||||||
|
keyCount int
|
||||||
|
src string
|
||||||
|
hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScript returns a new script object. If keyCount is greater than or equal
|
||||||
|
// to zero, then the count is automatically inserted in the EVAL command
|
||||||
|
// argument list. If keyCount is less than zero, then the application supplies
|
||||||
|
// the count as the first value in the keysAndArgs argument to the Do, Send and
|
||||||
|
// SendHash methods.
|
||||||
|
func NewScript(keyCount int, src string) *Script {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, src)
|
||||||
|
return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
|
||||||
|
var args []interface{}
|
||||||
|
if s.keyCount < 0 {
|
||||||
|
args = make([]interface{}, 1+len(keysAndArgs))
|
||||||
|
args[0] = spec
|
||||||
|
copy(args[1:], keysAndArgs)
|
||||||
|
} else {
|
||||||
|
args = make([]interface{}, 2+len(keysAndArgs))
|
||||||
|
args[0] = spec
|
||||||
|
args[1] = s.keyCount
|
||||||
|
copy(args[2:], keysAndArgs)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do evaluates the script. Under the covers, Do optimistically evaluates the
|
||||||
|
// script using the EVALSHA command. If the command fails because the script is
|
||||||
|
// not loaded, then Do evaluates the script using the EVAL command (thus
|
||||||
|
// causing the script to load).
|
||||||
|
func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
|
||||||
|
v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||||
|
if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
|
||||||
|
v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendHash evaluates the script without waiting for the reply. The script is
|
||||||
|
// evaluated with the EVALSHA command. The application must ensure that the
|
||||||
|
// script is loaded by a previous call to Send, Do or Load methods.
|
||||||
|
func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
|
||||||
|
return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send evaluates the script without waiting for the reply.
|
||||||
|
func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
|
||||||
|
return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads the script without evaluating it.
|
||||||
|
func (s *Script) Load(c Conn) error {
|
||||||
|
_, err := c.Do("SCRIPT", "LOAD", s.src)
|
||||||
|
return err
|
||||||
|
}
|
100
vendor/github.com/garyburd/redigo/redis/script_test.go
generated
vendored
Normal file
100
vendor/github.com/garyburd/redigo/redis/script_test.go
generated
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// These variables are declared at package level to remove distracting
|
||||||
|
// details from the examples.
|
||||||
|
c redis.Conn
|
||||||
|
reply interface{}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleScript() {
|
||||||
|
// Initialize a package-level variable with a script.
|
||||||
|
var getScript = redis.NewScript(1, `return redis.call('get', KEYS[1])`)
|
||||||
|
|
||||||
|
// In a function, use the script Do method to evaluate the script. The Do
|
||||||
|
// method optimistically uses the EVALSHA command. If the script is not
|
||||||
|
// loaded, then the Do method falls back to the EVAL command.
|
||||||
|
reply, err = getScript.Do(c, "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScript(t *testing.T) {
|
||||||
|
c, err := redis.DialDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// To test fall back in Do, we make script unique by adding comment with current time.
|
||||||
|
script := fmt.Sprintf("--%d\nreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", time.Now().UnixNano())
|
||||||
|
s := redis.NewScript(2, script)
|
||||||
|
reply := []interface{}{[]byte("key1"), []byte("key2"), []byte("arg1"), []byte("arg2")}
|
||||||
|
|
||||||
|
v, err := s.Do(c, "key1", "key2", "arg1", "arg2")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("s.Do(c, ...) returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(v, reply) {
|
||||||
|
t.Errorf("s.Do(c, ..); = %v, want %v", v, reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Load(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("s.Load(c) returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.SendHash(c, "key1", "key2", "arg1", "arg2")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("s.SendHash(c, ...) returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Flush()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("c.Flush() returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = c.Receive()
|
||||||
|
if !reflect.DeepEqual(v, reply) {
|
||||||
|
t.Errorf("s.SendHash(c, ..); c.Receive() = %v, want %v", v, reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Send(c, "key1", "key2", "arg1", "arg2")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("s.Send(c, ...) returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Flush()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("c.Flush() returned %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = c.Receive()
|
||||||
|
if !reflect.DeepEqual(v, reply) {
|
||||||
|
t.Errorf("s.Send(c, ..); c.Receive() = %v, want %v", v, reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
177
vendor/github.com/garyburd/redigo/redis/test_test.go
generated
vendored
Normal file
177
vendor/github.com/garyburd/redigo/redis/test_test.go
generated
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetNowFunc(f func() time.Time) {
|
||||||
|
nowFunc = f
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNegativeInt = errNegativeInt
|
||||||
|
|
||||||
|
serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary")
|
||||||
|
serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers")
|
||||||
|
serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`")
|
||||||
|
serverLog = ioutil.Discard
|
||||||
|
|
||||||
|
defaultServerMu sync.Mutex
|
||||||
|
defaultServer *Server
|
||||||
|
defaultServerErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
name string
|
||||||
|
cmd *exec.Cmd
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(name string, args ...string) (*Server, error) {
|
||||||
|
s := &Server{
|
||||||
|
name: name,
|
||||||
|
cmd: exec.Command(*serverPath, args...),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := s.cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ready := make(chan error, 1)
|
||||||
|
go s.watch(r, ready)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-ready:
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout waiting for server to start")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Stop()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) watch(r io.Reader, ready chan error) {
|
||||||
|
fmt.Fprintf(serverLog, "%d START %s \n", s.cmd.Process.Pid, s.name)
|
||||||
|
var listening bool
|
||||||
|
var text string
|
||||||
|
scn := bufio.NewScanner(r)
|
||||||
|
for scn.Scan() {
|
||||||
|
text = scn.Text()
|
||||||
|
fmt.Fprintf(serverLog, "%s\n", text)
|
||||||
|
if !listening {
|
||||||
|
if strings.Contains(text, "The server is now ready to accept connections on port") {
|
||||||
|
listening = true
|
||||||
|
ready <- nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !listening {
|
||||||
|
ready <- fmt.Errorf("server exited: %s", text)
|
||||||
|
}
|
||||||
|
s.cmd.Wait()
|
||||||
|
fmt.Fprintf(serverLog, "%d STOP %s \n", s.cmd.Process.Pid, s.name)
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
s.cmd.Process.Signal(os.Interrupt)
|
||||||
|
<-s.done
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopDefaultServer stops the server created by DialDefaultServer.
|
||||||
|
func stopDefaultServer() {
|
||||||
|
defaultServerMu.Lock()
|
||||||
|
defer defaultServerMu.Unlock()
|
||||||
|
if defaultServer != nil {
|
||||||
|
defaultServer.Stop()
|
||||||
|
defaultServer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startDefaultServer starts the default server if not already running.
|
||||||
|
func startDefaultServer() error {
|
||||||
|
defaultServerMu.Lock()
|
||||||
|
defer defaultServerMu.Unlock()
|
||||||
|
if defaultServer != nil || defaultServerErr != nil {
|
||||||
|
return defaultServerErr
|
||||||
|
}
|
||||||
|
defaultServer, defaultServerErr = NewServer(
|
||||||
|
"default",
|
||||||
|
"--port", strconv.Itoa(*serverBasePort),
|
||||||
|
"--save", "",
|
||||||
|
"--appendonly", "no")
|
||||||
|
return defaultServerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialDefaultServer starts the test server if not already started and dials a
|
||||||
|
// connection to the server.
|
||||||
|
func DialDefaultServer() (Conn, error) {
|
||||||
|
if err := startDefaultServer(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, err := Dial("tcp", fmt.Sprintf(":%d", *serverBasePort), DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Do("FLUSHDB")
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
os.Exit(func() int {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var f *os.File
|
||||||
|
if *serverLogName != "" {
|
||||||
|
var err error
|
||||||
|
f, err = os.OpenFile(*serverLogName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error opening redis-log: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
serverLog = f
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stopDefaultServer()
|
||||||
|
|
||||||
|
return m.Run()
|
||||||
|
}())
|
||||||
|
}
|
113
vendor/github.com/garyburd/redigo/redis/zpop_example_test.go
generated
vendored
Normal file
113
vendor/github.com/garyburd/redigo/redis/zpop_example_test.go
generated
vendored
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright 2013 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// zpop pops a value from the ZSET key using WATCH/MULTI/EXEC commands.
|
||||||
|
func zpop(c redis.Conn, key string) (result string, err error) {
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Return connection to normal state on error.
|
||||||
|
if err != nil {
|
||||||
|
c.Do("DISCARD")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Loop until transaction is successful.
|
||||||
|
for {
|
||||||
|
if _, err := c.Do("WATCH", key); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := redis.Strings(c.Do("ZRANGE", key, 0, 0))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(members) != 1 {
|
||||||
|
return "", redis.ErrNil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Send("MULTI")
|
||||||
|
c.Send("ZREM", key, members[0])
|
||||||
|
queued, err := c.Do("EXEC")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if queued != nil {
|
||||||
|
result = members[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// zpopScript pops a value from a ZSET.
|
||||||
|
var zpopScript = redis.NewScript(1, `
|
||||||
|
local r = redis.call('ZRANGE', KEYS[1], 0, 0)
|
||||||
|
if r ~= nil then
|
||||||
|
r = r[1]
|
||||||
|
redis.call('ZREM', KEYS[1], r)
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
`)
|
||||||
|
|
||||||
|
// This example implements ZPOP as described at
|
||||||
|
// http://redis.io/topics/transactions using WATCH/MULTI/EXEC and scripting.
|
||||||
|
func Example_zpop() {
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// Add test data using a pipeline.
|
||||||
|
|
||||||
|
for i, member := range []string{"red", "blue", "green"} {
|
||||||
|
c.Send("ZADD", "zset", i, member)
|
||||||
|
}
|
||||||
|
if _, err := c.Do(""); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop using WATCH/MULTI/EXEC
|
||||||
|
|
||||||
|
v, err := zpop(c, "zset")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(v)
|
||||||
|
|
||||||
|
// Pop using a script.
|
||||||
|
|
||||||
|
v, err = redis.String(zpopScript.Do(c, "zset"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(v)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// red
|
||||||
|
// blue
|
||||||
|
}
|
152
vendor/github.com/garyburd/redigo/redisx/connmux.go
generated
vendored
Normal file
152
vendor/github.com/garyburd/redigo/redisx/connmux.go
generated
vendored
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
// Copyright 2014 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redisx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/internal"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnMux multiplexes one or more connections to a single underlying
|
||||||
|
// connection. The ConnMux connections do not support concurrency, commands
|
||||||
|
// that associate server side state with the connection or commands that put
|
||||||
|
// the connection in a special mode.
|
||||||
|
type ConnMux struct {
|
||||||
|
c redis.Conn
|
||||||
|
|
||||||
|
sendMu sync.Mutex
|
||||||
|
sendID uint
|
||||||
|
|
||||||
|
recvMu sync.Mutex
|
||||||
|
recvID uint
|
||||||
|
recvWait map[uint]chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnMux(c redis.Conn) *ConnMux {
|
||||||
|
return &ConnMux{c: c, recvWait: make(map[uint]chan struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a connection. The application must close the returned connection.
|
||||||
|
func (p *ConnMux) Get() redis.Conn {
|
||||||
|
c := &muxConn{p: p}
|
||||||
|
c.ids = c.buf[:0]
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying connection.
|
||||||
|
func (p *ConnMux) Close() error {
|
||||||
|
return p.c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type muxConn struct {
|
||||||
|
p *ConnMux
|
||||||
|
ids []uint
|
||||||
|
buf [8]uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) send(flush bool, cmd string, args ...interface{}) error {
|
||||||
|
if internal.LookupCommandInfo(cmd).Set != 0 {
|
||||||
|
return errors.New("command not supported by mux pool")
|
||||||
|
}
|
||||||
|
p := c.p
|
||||||
|
p.sendMu.Lock()
|
||||||
|
id := p.sendID
|
||||||
|
c.ids = append(c.ids, id)
|
||||||
|
p.sendID++
|
||||||
|
err := p.c.Send(cmd, args...)
|
||||||
|
if flush {
|
||||||
|
err = p.c.Flush()
|
||||||
|
}
|
||||||
|
p.sendMu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) Send(cmd string, args ...interface{}) error {
|
||||||
|
return c.send(false, cmd, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) Flush() error {
|
||||||
|
p := c.p
|
||||||
|
p.sendMu.Lock()
|
||||||
|
err := p.c.Flush()
|
||||||
|
p.sendMu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) Receive() (interface{}, error) {
|
||||||
|
if len(c.ids) == 0 {
|
||||||
|
return nil, errors.New("mux pool underflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.ids[0]
|
||||||
|
c.ids = c.ids[1:]
|
||||||
|
if len(c.ids) == 0 {
|
||||||
|
c.ids = c.buf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
p := c.p
|
||||||
|
p.recvMu.Lock()
|
||||||
|
if p.recvID != id {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
p.recvWait[id] = ch
|
||||||
|
p.recvMu.Unlock()
|
||||||
|
<-ch
|
||||||
|
p.recvMu.Lock()
|
||||||
|
if p.recvID != id {
|
||||||
|
panic("out of sync")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := p.c.Receive()
|
||||||
|
|
||||||
|
id++
|
||||||
|
p.recvID = id
|
||||||
|
ch, ok := p.recvWait[id]
|
||||||
|
if ok {
|
||||||
|
delete(p.recvWait, id)
|
||||||
|
}
|
||||||
|
p.recvMu.Unlock()
|
||||||
|
if ok {
|
||||||
|
ch <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) Close() error {
|
||||||
|
var err error
|
||||||
|
if len(c.ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.Flush()
|
||||||
|
for _ = range c.ids {
|
||||||
|
_, err = c.Receive()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) Do(cmd string, args ...interface{}) (interface{}, error) {
|
||||||
|
if err := c.send(true, cmd, args...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Receive()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *muxConn) Err() error {
|
||||||
|
return c.p.c.Err()
|
||||||
|
}
|
259
vendor/github.com/garyburd/redigo/redisx/connmux_test.go
generated
vendored
Normal file
259
vendor/github.com/garyburd/redigo/redisx/connmux_test.go
generated
vendored
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
// Copyright 2014 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redisx_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/textproto"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/internal/redistest"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/garyburd/redigo/redisx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConnMux(t *testing.T) {
|
||||||
|
c, err := redistest.Dial()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
m := redisx.NewConnMux(c)
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
c1 := m.Get()
|
||||||
|
c2 := m.Get()
|
||||||
|
c1.Send("ECHO", "hello")
|
||||||
|
c2.Send("ECHO", "world")
|
||||||
|
c1.Flush()
|
||||||
|
c2.Flush()
|
||||||
|
s, err := redis.String(c1.Receive())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s != "hello" {
|
||||||
|
t.Fatalf("echo returned %q, want %q", s, "hello")
|
||||||
|
}
|
||||||
|
s, err = redis.String(c2.Receive())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s != "world" {
|
||||||
|
t.Fatalf("echo returned %q, want %q", s, "world")
|
||||||
|
}
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnMuxClose(t *testing.T) {
|
||||||
|
c, err := redistest.Dial()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
m := redisx.NewConnMux(c)
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
c1 := m.Get()
|
||||||
|
c2 := m.Get()
|
||||||
|
|
||||||
|
if err := c1.Send("ECHO", "hello"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := c1.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c2.Send("ECHO", "world"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := c2.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := redis.String(c2.Receive())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s != "world" {
|
||||||
|
t.Fatalf("echo returned %q, want %q", s, "world")
|
||||||
|
}
|
||||||
|
c2.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkConn(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
c, err := redistest.Dial()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkConnMux(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
c, err := redistest.Dial()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
m := redisx.NewConnMux(c)
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := m.Get()
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPool(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
p := redis.Pool{Dial: redistest.Dial, MaxIdle: 1}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// Fill the pool.
|
||||||
|
c := p.Get()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := p.Get()
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numConcurrent = 10
|
||||||
|
|
||||||
|
func BenchmarkConnMuxConcurrent(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
c, err := redistest.Dial()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
m := redisx.NewConnMux(c)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(numConcurrent)
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < numConcurrent; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := m.Get()
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPoolConcurrent(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
p := redis.Pool{Dial: redistest.Dial, MaxIdle: numConcurrent}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// Fill the pool.
|
||||||
|
conns := make([]redis.Conn, numConcurrent)
|
||||||
|
for i := range conns {
|
||||||
|
c := p.Get()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
conns[i] = c
|
||||||
|
}
|
||||||
|
for _, c := range conns {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(numConcurrent)
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < numConcurrent; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
c := p.Get()
|
||||||
|
if _, err := c.Do("PING"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPipelineConcurrency(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
c, err := redistest.Dial()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("error connection to database, %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(numConcurrent)
|
||||||
|
|
||||||
|
var pipeline textproto.Pipeline
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < numConcurrent; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
id := pipeline.Next()
|
||||||
|
pipeline.StartRequest(id)
|
||||||
|
c.Send("PING")
|
||||||
|
c.Flush()
|
||||||
|
pipeline.EndRequest(id)
|
||||||
|
pipeline.StartResponse(id)
|
||||||
|
_, err := c.Receive()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
pipeline.EndResponse(id)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
17
vendor/github.com/garyburd/redigo/redisx/doc.go
generated
vendored
Normal file
17
vendor/github.com/garyburd/redigo/redisx/doc.go
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
// Package redisx contains experimental features for Redigo. Features in this
|
||||||
|
// package may be modified or deleted at any time.
|
||||||
|
package redisx // import "github.com/garyburd/redigo/redisx"
|
27
vendor/github.com/gin-contrib/multitemplate/.gitignore
generated
vendored
Normal file
27
vendor/github.com/gin-contrib/multitemplate/.gitignore
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
vendor/*
|
||||||
|
!vendor/vendor.json
|
||||||
|
coverage.out
|
30
vendor/github.com/gin-contrib/multitemplate/.travis.yml
generated
vendored
Normal file
30
vendor/github.com/gin-contrib/multitemplate/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.6.x
|
||||||
|
- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
- 1.9.x
|
||||||
|
- tip
|
||||||
|
|
||||||
|
install:
|
||||||
|
- make install
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make vet
|
||||||
|
- make fmt-check
|
||||||
|
- make embedmd
|
||||||
|
- make misspell-check
|
||||||
|
- make test
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks:
|
||||||
|
urls:
|
||||||
|
- https://webhooks.gitter.im/e/acc2c57482e94b44f557
|
||||||
|
on_success: change
|
||||||
|
on_failure: always
|
||||||
|
on_start: false
|
21
vendor/github.com/gin-contrib/multitemplate/LICENSE
generated
vendored
Normal file
21
vendor/github.com/gin-contrib/multitemplate/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 gin-contrib
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
61
vendor/github.com/gin-contrib/multitemplate/Makefile
generated
vendored
Normal file
61
vendor/github.com/gin-contrib/multitemplate/Makefile
generated
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
GOFMT ?= gofmt "-s"
|
||||||
|
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
|
||||||
|
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
install: deps
|
||||||
|
govendor sync
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -v -covermode=count -coverprofile=coverage.out
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt:
|
||||||
|
$(GOFMT) -w $(GOFILES)
|
||||||
|
|
||||||
|
.PHONY: fmt-check
|
||||||
|
fmt-check:
|
||||||
|
# get all go files and run go fmt on them
|
||||||
|
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
||||||
|
if [ -n "$$diff" ]; then \
|
||||||
|
echo "Please run 'make fmt' and commit the result:"; \
|
||||||
|
echo "$${diff}"; \
|
||||||
|
exit 1; \
|
||||||
|
fi;
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet $(PACKAGES)
|
||||||
|
|
||||||
|
deps:
|
||||||
|
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/kardianos/govendor; \
|
||||||
|
fi
|
||||||
|
@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/campoy/embedmd; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
embedmd:
|
||||||
|
embedmd -d *.md
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/golang/lint/golint; \
|
||||||
|
fi
|
||||||
|
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
||||||
|
|
||||||
|
.PHONY: misspell-check
|
||||||
|
misspell-check:
|
||||||
|
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/client9/misspell/cmd/misspell; \
|
||||||
|
fi
|
||||||
|
misspell -error $(GOFILES)
|
||||||
|
|
||||||
|
.PHONY: misspell
|
||||||
|
misspell:
|
||||||
|
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/client9/misspell/cmd/misspell; \
|
||||||
|
fi
|
||||||
|
misspell -w $(GOFILES)
|
109
vendor/github.com/gin-contrib/multitemplate/README.md
generated
vendored
Normal file
109
vendor/github.com/gin-contrib/multitemplate/README.md
generated
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Multitemplate
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/gin-contrib/multitemplate.svg)](https://travis-ci.org/gin-contrib/multitemplate)
|
||||||
|
[![codecov](https://codecov.io/gh/gin-contrib/multitemplate/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/multitemplate)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/multitemplate)](https://goreportcard.com/report/github.com/gin-contrib/multitemplate)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gin-contrib/multitemplate?status.svg)](https://godoc.org/github.com/gin-contrib/multitemplate)
|
||||||
|
|
||||||
|
This is a custom HTML render to support multi templates, ie. more than one `*template.Template`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Start using it
|
||||||
|
|
||||||
|
Download and install it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go get github.com/gin-contrib/multitemplate
|
||||||
|
```
|
||||||
|
|
||||||
|
Import it in your code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/gin-contrib/multitemplate"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simple example
|
||||||
|
|
||||||
|
See [example/example.go](example/example.go)
|
||||||
|
|
||||||
|
[embedmd]:# (example/example.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/multitemplate"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createMyRender() multitemplate.Render {
|
||||||
|
r := multitemplate.New()
|
||||||
|
r.AddFromFiles("index", "templates/base.html", "templates/index.html")
|
||||||
|
r.AddFromFiles("article", "templates/base.html", "templates/index.html", "templates/article.html")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
router.HTMLRender = createMyRender()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"title": "Html5 Template Engine",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
router.GET("/article", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "article", gin.H{
|
||||||
|
"title": "Html5 Article Engine",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced example
|
||||||
|
|
||||||
|
[Approximating html/template Inheritance](https://elithrar.github.io/article/approximating-html-template-inheritance/)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/multitemplate"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
router.HTMLRender = loadTemplates("./templates")
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index.tmpl", gin.H{
|
||||||
|
"title": "Welcome!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplates(templatesDir string) multitemplate.Render {
|
||||||
|
r := multitemplate.New()
|
||||||
|
|
||||||
|
layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate our templates map from our layouts/ and includes/ directories
|
||||||
|
for _, layout := range layouts {
|
||||||
|
files := append([]string{layout}, includes...)
|
||||||
|
r.Add(filepath.Base(layout), template.Must(template.ParseFiles(files...)))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
```
|
29
vendor/github.com/gin-contrib/multitemplate/example/example.go
generated
vendored
Normal file
29
vendor/github.com/gin-contrib/multitemplate/example/example.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/multitemplate"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createMyRender() multitemplate.Render {
|
||||||
|
r := multitemplate.New()
|
||||||
|
r.AddFromFiles("index", "templates/base.html", "templates/index.html")
|
||||||
|
r.AddFromFiles("article", "templates/base.html", "templates/index.html", "templates/article.html")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
router.HTMLRender = createMyRender()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"title": "Html5 Template Engine",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
router.GET("/article", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "article", gin.H{
|
||||||
|
"title": "Html5 Article Engine",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
1
vendor/github.com/gin-contrib/multitemplate/example/templates/article.html
generated
vendored
Normal file
1
vendor/github.com/gin-contrib/multitemplate/example/templates/article.html
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{define "article.html"}}Hi, this is article template{{end}}
|
3
vendor/github.com/gin-contrib/multitemplate/example/templates/base.html
generated
vendored
Normal file
3
vendor/github.com/gin-contrib/multitemplate/example/templates/base.html
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Title: {{ .title }}
|
||||||
|
|
||||||
|
index template: {{template "index.html"}}
|
1
vendor/github.com/gin-contrib/multitemplate/example/templates/index.html
generated
vendored
Normal file
1
vendor/github.com/gin-contrib/multitemplate/example/templates/index.html
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hi, this is index.html
|
78
vendor/github.com/gin-contrib/multitemplate/multitemplate.go
generated
vendored
Normal file
78
vendor/github.com/gin-contrib/multitemplate/multitemplate.go
generated
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package multitemplate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Render type
|
||||||
|
type Render map[string]*template.Template
|
||||||
|
|
||||||
|
var _ render.HTMLRender = Render{}
|
||||||
|
|
||||||
|
// New instance
|
||||||
|
func New() Render {
|
||||||
|
return make(Render)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new template
|
||||||
|
func (r Render) Add(name string, tmpl *template.Template) {
|
||||||
|
if tmpl == nil {
|
||||||
|
panic("template can not be nil")
|
||||||
|
}
|
||||||
|
if len(name) == 0 {
|
||||||
|
panic("template name cannot be empty")
|
||||||
|
}
|
||||||
|
r[name] = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFromFiles supply add template from files
|
||||||
|
func (r Render) AddFromFiles(name string, files ...string) *template.Template {
|
||||||
|
tmpl := template.Must(template.ParseFiles(files...))
|
||||||
|
r.Add(name, tmpl)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFromGlob supply add template from global path
|
||||||
|
func (r Render) AddFromGlob(name, glob string) *template.Template {
|
||||||
|
tmpl := template.Must(template.ParseGlob(glob))
|
||||||
|
r.Add(name, tmpl)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFromString supply add template from strings
|
||||||
|
func (r *Render) AddFromString(name, templateString string) *template.Template {
|
||||||
|
tmpl := template.Must(template.New(name).Parse(templateString))
|
||||||
|
r.Add(name, tmpl)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFromStringsFuncs supply add template from strings
|
||||||
|
func (r *Render) AddFromStringsFuncs(name string, funcMap template.FuncMap, templateStrings ...string) *template.Template {
|
||||||
|
tmpl := template.New(name).Funcs(funcMap)
|
||||||
|
|
||||||
|
for _, ts := range templateStrings {
|
||||||
|
tmpl = template.Must(tmpl.Parse(ts))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Add(name, tmpl)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFromFilesFuncs supply add template from file callback func
|
||||||
|
func (r Render) AddFromFilesFuncs(name string, funcMap template.FuncMap, files ...string) *template.Template {
|
||||||
|
tname := filepath.Base(files[0])
|
||||||
|
tmpl := template.Must(template.New(tname).Funcs(funcMap).ParseFiles(files...))
|
||||||
|
r.Add(name, tmpl)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance supply render string
|
||||||
|
func (r Render) Instance(name string, data interface{}) render.Render {
|
||||||
|
return render.HTML{
|
||||||
|
Template: r[name],
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
142
vendor/github.com/gin-contrib/multitemplate/multitemplate_test.go
generated
vendored
Normal file
142
vendor/github.com/gin-contrib/multitemplate/multitemplate_test.go
generated
vendored
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package multitemplate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||||
|
req, _ := http.NewRequest(method, path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAsDate(t time.Time) string {
|
||||||
|
year, month, day := t.Date()
|
||||||
|
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFromFile() Render {
|
||||||
|
r := New()
|
||||||
|
r.AddFromFiles("index", "tests/base.html", "tests/article.html")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFromGlob() Render {
|
||||||
|
r := New()
|
||||||
|
r.AddFromGlob("index", "tests/global/*")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFromString() Render {
|
||||||
|
r := New()
|
||||||
|
r.AddFromString("index", "Welcome to {{ .name }} template")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFromStringsWithFuncs() Render {
|
||||||
|
r := New()
|
||||||
|
r.AddFromStringsFuncs("index", template.FuncMap{}, `Welcome to {{ .name }} {{template "content"}}`, `{{define "content"}}template{{end}}`)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFromFilesWithFuncs() Render {
|
||||||
|
r := New()
|
||||||
|
r.AddFromFilesFuncs("index", template.FuncMap{}, "tests/welcome.html", "tests/content.html")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingTemplateOrName(t *testing.T) {
|
||||||
|
r := New()
|
||||||
|
tmpl := template.Must(template.New("test").Parse("Welcome to {{ .name }} template"))
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
r.Add("", tmpl)
|
||||||
|
}, "template name cannot be empty")
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
r.Add("test", nil)
|
||||||
|
}, "template can not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFromFiles(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.HTMLRender = createFromFile()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"title": "Test Multiple Template",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "<p>Test Multiple Template</p>\nHi, this is article template\n", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFromGlob(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.HTMLRender = createFromGlob()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"title": "Test Multiple Template",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "<p>Test Multiple Template</p>\nHi, this is login template\n", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFromString(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.HTMLRender = createFromString()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"name": "index",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "Welcome to index template", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFromStringsFruncs(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.HTMLRender = createFromStringsWithFuncs()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"name": "index",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "Welcome to index template", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFromFilesFruncs(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.HTMLRender = createFromFilesWithFuncs()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(200, "index", gin.H{
|
||||||
|
"name": "index",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "Welcome to index template\n", w.Body.String())
|
||||||
|
}
|
1
vendor/github.com/gin-contrib/multitemplate/tests/article.html
generated
vendored
Normal file
1
vendor/github.com/gin-contrib/multitemplate/tests/article.html
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{define "article.html"}}Hi, this is article template{{end}}
|
2
vendor/github.com/gin-contrib/multitemplate/tests/base.html
generated
vendored
Normal file
2
vendor/github.com/gin-contrib/multitemplate/tests/base.html
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<p>{{ .title }}</p>
|
||||||
|
{{template "article.html"}}
|
1
vendor/github.com/gin-contrib/multitemplate/tests/content.html
generated
vendored
Normal file
1
vendor/github.com/gin-contrib/multitemplate/tests/content.html
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{define "content"}}template{{end}}
|
2
vendor/github.com/gin-contrib/multitemplate/tests/global/base.html
generated
vendored
Normal file
2
vendor/github.com/gin-contrib/multitemplate/tests/global/base.html
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<p>{{ .title }}</p>
|
||||||
|
{{template "login.html"}}
|
1
vendor/github.com/gin-contrib/multitemplate/tests/global/login.html
generated
vendored
Normal file
1
vendor/github.com/gin-contrib/multitemplate/tests/global/login.html
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{define "login.html"}}Hi, this is login template{{end}}
|
1
vendor/github.com/gin-contrib/multitemplate/tests/welcome.html
generated
vendored
Normal file
1
vendor/github.com/gin-contrib/multitemplate/tests/welcome.html
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Welcome to {{ .name }} {{template "content"}}
|
3
vendor/github.com/gin-contrib/sessions/.gitignore
generated
vendored
Normal file
3
vendor/github.com/gin-contrib/sessions/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
coverage.out
|
||||||
|
vendor/*
|
||||||
|
!/vendor/vendor.json
|
34
vendor/github.com/gin-contrib/sessions/.travis.yml
generated
vendored
Normal file
34
vendor/github.com/gin-contrib/sessions/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.6.x
|
||||||
|
- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
- 1.9.x
|
||||||
|
- tip
|
||||||
|
|
||||||
|
services:
|
||||||
|
- redis
|
||||||
|
- memcached
|
||||||
|
- mongodb
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get -u github.com/kardianos/govendor
|
||||||
|
- go get github.com/campoy/embedmd
|
||||||
|
- govendor sync
|
||||||
|
|
||||||
|
script:
|
||||||
|
- embedmd -d *.md
|
||||||
|
- go test -v -covermode=atomic -coverprofile=coverage.out
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks:
|
||||||
|
urls:
|
||||||
|
- https://webhooks.gitter.im/e/acc2c57482e94b44f557
|
||||||
|
on_success: change
|
||||||
|
on_failure: always
|
||||||
|
on_start: false
|
21
vendor/github.com/gin-contrib/sessions/LICENSE
generated
vendored
Normal file
21
vendor/github.com/gin-contrib/sessions/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 Gin-Gonic
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
172
vendor/github.com/gin-contrib/sessions/README.md
generated
vendored
Normal file
172
vendor/github.com/gin-contrib/sessions/README.md
generated
vendored
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# sessions
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/gin-contrib/sessions.svg)](https://travis-ci.org/gin-contrib/sessions)
|
||||||
|
[![codecov](https://codecov.io/gh/gin-contrib/sessions/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/sessions)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/sessions)](https://goreportcard.com/report/github.com/gin-contrib/sessions)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gin-contrib/sessions?status.svg)](https://godoc.org/github.com/gin-contrib/sessions)
|
||||||
|
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin)
|
||||||
|
|
||||||
|
Gin middleware for session management with multi-backend support (currently cookie, Redis, Memcached, MongoDB).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Start using it
|
||||||
|
|
||||||
|
Download and install it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go get github.com/gin-contrib/sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
Import it in your code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/gin-contrib/sessions"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
#### cookie-based
|
||||||
|
|
||||||
|
[embedmd]:# (example_cookie/main.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
store := sessions.NewCookieStore([]byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Redis
|
||||||
|
|
||||||
|
[embedmd]:# (example_redis/main.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
store, _ := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Memcached
|
||||||
|
|
||||||
|
[embedmd]:# (example_memcached/main.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
store := sessions.NewMemcacheStore(memcache.New("localhost:11211"), "", []byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### MongoDB
|
||||||
|
|
||||||
|
[embedmd]:# (example_mongo/main.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gopkg.in/mgo.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
session, err := mgo.Dial("localhost:27017/test")
|
||||||
|
if err != nil {
|
||||||
|
// handle err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := session.DB("").C("sessions")
|
||||||
|
store := sessions.NewMongoStore(c, 3600, true, []byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
||||||
|
```
|
36
vendor/github.com/gin-contrib/sessions/cookie.go
generated
vendored
Normal file
36
vendor/github.com/gin-contrib/sessions/cookie.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CookieStore interface {
|
||||||
|
Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys are defined in pairs to allow key rotation, but the common case is to set a single
|
||||||
|
// authentication key and optionally an encryption key.
|
||||||
|
//
|
||||||
|
// The first key in a pair is used for authentication and the second for encryption. The
|
||||||
|
// encryption key can be set to nil or omitted in the last pair, but the authentication key
|
||||||
|
// is required in all pairs.
|
||||||
|
//
|
||||||
|
// It is recommended to use an authentication key with 32 or 64 bytes. The encryption key,
|
||||||
|
// if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes.
|
||||||
|
func NewCookieStore(keyPairs ...[]byte) CookieStore {
|
||||||
|
return &cookieStore{sessions.NewCookieStore(keyPairs...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cookieStore struct {
|
||||||
|
*sessions.CookieStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cookieStore) Options(options Options) {
|
||||||
|
c.CookieStore.Options = &sessions.Options{
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
}
|
30
vendor/github.com/gin-contrib/sessions/cookie_test.go
generated
vendored
Normal file
30
vendor/github.com/gin-contrib/sessions/cookie_test.go
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var newCookieStore = func(_ *testing.T) Store {
|
||||||
|
store := NewCookieStore([]byte("secret"))
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookie_SessionGetSet(t *testing.T) {
|
||||||
|
sessionGetSet(t, newCookieStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookie_SessionDeleteKey(t *testing.T) {
|
||||||
|
sessionDeleteKey(t, newCookieStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookie_SessionFlashes(t *testing.T) {
|
||||||
|
sessionFlashes(t, newCookieStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookie_SessionClear(t *testing.T) {
|
||||||
|
sessionClear(t, newCookieStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookie_SessionOptions(t *testing.T) {
|
||||||
|
sessionOptions(t, newCookieStore)
|
||||||
|
}
|
28
vendor/github.com/gin-contrib/sessions/example_cookie/main.go
generated
vendored
Normal file
28
vendor/github.com/gin-contrib/sessions/example_cookie/main.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
store := sessions.NewCookieStore([]byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
29
vendor/github.com/gin-contrib/sessions/example_memcached/main.go
generated
vendored
Normal file
29
vendor/github.com/gin-contrib/sessions/example_memcached/main.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
store := sessions.NewMemcacheStore(memcache.New("localhost:11211"), "", []byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
35
vendor/github.com/gin-contrib/sessions/example_mongo/main.go
generated
vendored
Normal file
35
vendor/github.com/gin-contrib/sessions/example_mongo/main.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gopkg.in/mgo.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
session, err := mgo.Dial("localhost:27017/test")
|
||||||
|
if err != nil {
|
||||||
|
// handle err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := session.DB("").C("sessions")
|
||||||
|
store := sessions.NewMongoStore(c, 3600, true, []byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
28
vendor/github.com/gin-contrib/sessions/example_redis/main.go
generated
vendored
Normal file
28
vendor/github.com/gin-contrib/sessions/example_redis/main.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
store, _ := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("secret"))
|
||||||
|
r.Use(sessions.Sessions("mysession", store))
|
||||||
|
|
||||||
|
r.GET("/incr", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
var count int
|
||||||
|
v := session.Get("count")
|
||||||
|
if v == nil {
|
||||||
|
count = 0
|
||||||
|
} else {
|
||||||
|
count = v.(int)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
session.Set("count", count)
|
||||||
|
session.Save()
|
||||||
|
c.JSON(200, gin.H{"count": count})
|
||||||
|
})
|
||||||
|
r.Run(":8000")
|
||||||
|
}
|
32
vendor/github.com/gin-contrib/sessions/memcached.go
generated
vendored
Normal file
32
vendor/github.com/gin-contrib/sessions/memcached.go
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"github.com/bradleypeabody/gorilla-sessions-memcache"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemcachedStore interface {
|
||||||
|
Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// client: memcache client.
|
||||||
|
// keyPrefix: prefix for the keys we store.
|
||||||
|
func NewMemcacheStore(client *memcache.Client, keyPrefix string, keyPairs ...[]byte) MemcachedStore {
|
||||||
|
store := gsm.NewMemcacheStore(client, keyPrefix, keyPairs...)
|
||||||
|
return &memcacheStore{store}
|
||||||
|
}
|
||||||
|
|
||||||
|
type memcacheStore struct {
|
||||||
|
*gsm.MemcacheStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memcacheStore) Options(options Options) {
|
||||||
|
c.MemcacheStore.Options = &sessions.Options{
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
}
|
33
vendor/github.com/gin-contrib/sessions/memcached_test.go
generated
vendored
Normal file
33
vendor/github.com/gin-contrib/sessions/memcached_test.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const memcachedTestServer = "localhost:11211"
|
||||||
|
|
||||||
|
var newMemcachedStore = func(_ *testing.T) Store {
|
||||||
|
store := NewMemcacheStore(memcache.New(memcachedTestServer), "", []byte("secret"))
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemcached_SessionGetSet(t *testing.T) {
|
||||||
|
sessionGetSet(t, newMemcachedStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemcached_SessionDeleteKey(t *testing.T) {
|
||||||
|
sessionDeleteKey(t, newMemcachedStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemcached_SessionFlashes(t *testing.T) {
|
||||||
|
sessionFlashes(t, newMemcachedStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemcached_SessionClear(t *testing.T) {
|
||||||
|
sessionClear(t, newMemcachedStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemcached_SessionOptions(t *testing.T) {
|
||||||
|
sessionOptions(t, newMemcachedStore)
|
||||||
|
}
|
31
vendor/github.com/gin-contrib/sessions/mongo.go
generated
vendored
Normal file
31
vendor/github.com/gin-contrib/sessions/mongo.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/kidstuff/mongostore"
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MongoStore interface {
|
||||||
|
Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMongoStore(c *mgo.Collection, maxAge int, ensureTTL bool, keyPairs ...[]byte) MongoStore {
|
||||||
|
store := mongostore.NewMongoStore(c, maxAge, ensureTTL, keyPairs...)
|
||||||
|
|
||||||
|
return &mongoStore{store}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mongoStore struct {
|
||||||
|
*mongostore.MongoStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mongoStore) Options(options Options) {
|
||||||
|
c.MongoStore.Options = &sessions.Options{
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
}
|
39
vendor/github.com/gin-contrib/sessions/mongo_test.go
generated
vendored
Normal file
39
vendor/github.com/gin-contrib/sessions/mongo_test.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mongoTestServer = "localhost:27017"
|
||||||
|
|
||||||
|
var newMongoStore = func(_ *testing.T) Store {
|
||||||
|
session, err := mgo.Dial(mongoTestServer)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := session.DB("test").C("sessions")
|
||||||
|
return NewMongoStore(c, 3600, true, []byte("secret"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMongo_SessionGetSet(t *testing.T) {
|
||||||
|
sessionGetSet(t, newMongoStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMongo_SessionDeleteKey(t *testing.T) {
|
||||||
|
sessionDeleteKey(t, newMongoStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMongo_SessionFlashes(t *testing.T) {
|
||||||
|
sessionFlashes(t, newMongoStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMongo_SessionClear(t *testing.T) {
|
||||||
|
sessionClear(t, newMongoStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMongo_SessionOptions(t *testing.T) {
|
||||||
|
sessionOptions(t, newMongoStore)
|
||||||
|
}
|
69
vendor/github.com/gin-contrib/sessions/redis.go
generated
vendored
Normal file
69
vendor/github.com/gin-contrib/sessions/redis.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/boj/redistore"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisStore interface {
|
||||||
|
Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// size: maximum number of idle connections.
|
||||||
|
// network: tcp or udp
|
||||||
|
// address: host:port
|
||||||
|
// password: redis-password
|
||||||
|
// Keys are defined in pairs to allow key rotation, but the common case is to set a single
|
||||||
|
// authentication key and optionally an encryption key.
|
||||||
|
//
|
||||||
|
// The first key in a pair is used for authentication and the second for encryption. The
|
||||||
|
// encryption key can be set to nil or omitted in the last pair, but the authentication key
|
||||||
|
// is required in all pairs.
|
||||||
|
//
|
||||||
|
// It is recommended to use an authentication key with 32 or 64 bytes. The encryption key,
|
||||||
|
// if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes.
|
||||||
|
func NewRedisStore(size int, network, address, password string, keyPairs ...[]byte) (RedisStore, error) {
|
||||||
|
store, err := redistore.NewRediStore(size, network, address, password, keyPairs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &redisStore{store}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisStoreWithDB - like NewRedisStore but accepts `DB` parameter to select
|
||||||
|
// redis DB instead of using the default one ("0")
|
||||||
|
//
|
||||||
|
// Ref: https://godoc.org/github.com/boj/redistore#NewRediStoreWithDB
|
||||||
|
func NewRedisStoreWithDB(size int, network, address, password, DB string, keyPairs ...[]byte) (RedisStore, error) {
|
||||||
|
store, err := redistore.NewRediStoreWithDB(size, network, address, password, DB, keyPairs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &redisStore{store}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisStoreWithPool instantiates a RediStore with a *redis.Pool passed in.
|
||||||
|
//
|
||||||
|
// Ref: https://godoc.org/github.com/boj/redistore#NewRediStoreWithPool
|
||||||
|
func NewRedisStoreWithPool(pool *redis.Pool, keyPairs ...[]byte) (RedisStore, error) {
|
||||||
|
store, err := redistore.NewRediStoreWithPool(pool, keyPairs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &redisStore{store}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type redisStore struct {
|
||||||
|
*redistore.RediStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisStore) Options(options Options) {
|
||||||
|
c.RediStore.Options = &sessions.Options{
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
}
|
35
vendor/github.com/gin-contrib/sessions/redis_test.go
generated
vendored
Normal file
35
vendor/github.com/gin-contrib/sessions/redis_test.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const redisTestServer = "localhost:6379"
|
||||||
|
|
||||||
|
var newRedisStore = func(_ *testing.T) Store {
|
||||||
|
store, err := NewRedisStore(10, "tcp", redisTestServer, "", []byte("secret"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedis_SessionGetSet(t *testing.T) {
|
||||||
|
sessionGetSet(t, newRedisStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedis_SessionDeleteKey(t *testing.T) {
|
||||||
|
sessionDeleteKey(t, newRedisStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedis_SessionFlashes(t *testing.T) {
|
||||||
|
sessionFlashes(t, newRedisStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedis_SessionClear(t *testing.T) {
|
||||||
|
sessionClear(t, newRedisStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedis_SessionOptions(t *testing.T) {
|
||||||
|
sessionOptions(t, newRedisStore)
|
||||||
|
}
|
147
vendor/github.com/gin-contrib/sessions/sessions.go
generated
vendored
Normal file
147
vendor/github.com/gin-contrib/sessions/sessions.go
generated
vendored
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/context"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultKey = "github.com/gin-contrib/sessions"
|
||||||
|
errorFormat = "[sessions] ERROR! %s\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
sessions.Store
|
||||||
|
Options(Options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options stores configuration for a session or session store.
|
||||||
|
// Fields are a subset of http.Cookie fields.
|
||||||
|
type Options struct {
|
||||||
|
Path string
|
||||||
|
Domain string
|
||||||
|
// MaxAge=0 means no 'Max-Age' attribute specified.
|
||||||
|
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'.
|
||||||
|
// MaxAge>0 means Max-Age attribute present and given in seconds.
|
||||||
|
MaxAge int
|
||||||
|
Secure bool
|
||||||
|
HttpOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wraps thinly gorilla-session methods.
|
||||||
|
// Session stores the values and optional configuration for a session.
|
||||||
|
type Session interface {
|
||||||
|
// Get returns the session value associated to the given key.
|
||||||
|
Get(key interface{}) interface{}
|
||||||
|
// Set sets the session value associated to the given key.
|
||||||
|
Set(key interface{}, val interface{})
|
||||||
|
// Delete removes the session value associated to the given key.
|
||||||
|
Delete(key interface{})
|
||||||
|
// Clear deletes all values in the session.
|
||||||
|
Clear()
|
||||||
|
// AddFlash adds a flash message to the session.
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines the flash key.
|
||||||
|
// If not defined "_flash" is used by default.
|
||||||
|
AddFlash(value interface{}, vars ...string)
|
||||||
|
// Flashes returns a slice of flash messages from the session.
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines the flash key.
|
||||||
|
// If not defined "_flash" is used by default.
|
||||||
|
Flashes(vars ...string) []interface{}
|
||||||
|
// Options sets confuguration for a session.
|
||||||
|
Options(Options)
|
||||||
|
// Save saves all sessions used during the current request.
|
||||||
|
Save() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sessions(name string, store Store) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
s := &session{name, c.Request, store, nil, false, c.Writer}
|
||||||
|
c.Set(DefaultKey, s)
|
||||||
|
defer context.Clear(c.Request)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
name string
|
||||||
|
request *http.Request
|
||||||
|
store Store
|
||||||
|
session *sessions.Session
|
||||||
|
written bool
|
||||||
|
writer http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Get(key interface{}) interface{} {
|
||||||
|
return s.Session().Values[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Set(key interface{}, val interface{}) {
|
||||||
|
s.Session().Values[key] = val
|
||||||
|
s.written = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Delete(key interface{}) {
|
||||||
|
delete(s.Session().Values, key)
|
||||||
|
s.written = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Clear() {
|
||||||
|
for key := range s.Session().Values {
|
||||||
|
s.Delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) AddFlash(value interface{}, vars ...string) {
|
||||||
|
s.Session().AddFlash(value, vars...)
|
||||||
|
s.written = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Flashes(vars ...string) []interface{} {
|
||||||
|
s.written = true
|
||||||
|
return s.Session().Flashes(vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Options(options Options) {
|
||||||
|
s.Session().Options = &sessions.Options{
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Save() error {
|
||||||
|
if s.Written() {
|
||||||
|
e := s.Session().Save(s.request, s.writer)
|
||||||
|
if e == nil {
|
||||||
|
s.written = false
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Session() *sessions.Session {
|
||||||
|
if s.session == nil {
|
||||||
|
var err error
|
||||||
|
s.session, err = s.store.Get(s.request, s.name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(errorFormat, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Written() bool {
|
||||||
|
return s.written
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortcut to get session
|
||||||
|
func Default(c *gin.Context) Session {
|
||||||
|
return c.MustGet(DefaultKey).(Session)
|
||||||
|
}
|
220
vendor/github.com/gin-contrib/sessions/sessions_test.go
generated
vendored
Normal file
220
vendor/github.com/gin-contrib/sessions/sessions_test.go
generated
vendored
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storeFactory func(*testing.T) Store
|
||||||
|
|
||||||
|
const sessionName = "mysession"
|
||||||
|
|
||||||
|
const ok = "ok"
|
||||||
|
|
||||||
|
func sessionGetSet(t *testing.T, newStore storeFactory) {
|
||||||
|
r := gin.Default()
|
||||||
|
r.Use(Sessions(sessionName, newStore(t)))
|
||||||
|
|
||||||
|
r.GET("/set", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
session.Set("key", ok)
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/get", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
if session.Get("key") != ok {
|
||||||
|
t.Error("Session writing failed")
|
||||||
|
}
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
res1 := httptest.NewRecorder()
|
||||||
|
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||||
|
r.ServeHTTP(res1, req1)
|
||||||
|
|
||||||
|
res2 := httptest.NewRecorder()
|
||||||
|
req2, _ := http.NewRequest("GET", "/get", nil)
|
||||||
|
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||||
|
r.ServeHTTP(res2, req2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionDeleteKey(t *testing.T, newStore storeFactory) {
|
||||||
|
r := gin.Default()
|
||||||
|
r.Use(Sessions(sessionName, newStore(t)))
|
||||||
|
|
||||||
|
r.GET("/set", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
session.Set("key", ok)
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/delete", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
session.Delete("key")
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/get", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
if session.Get("key") != nil {
|
||||||
|
t.Error("Session deleting failed")
|
||||||
|
}
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
res1 := httptest.NewRecorder()
|
||||||
|
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||||
|
r.ServeHTTP(res1, req1)
|
||||||
|
|
||||||
|
res2 := httptest.NewRecorder()
|
||||||
|
req2, _ := http.NewRequest("GET", "/delete", nil)
|
||||||
|
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||||
|
r.ServeHTTP(res2, req2)
|
||||||
|
|
||||||
|
res3 := httptest.NewRecorder()
|
||||||
|
req3, _ := http.NewRequest("GET", "/get", nil)
|
||||||
|
req3.Header.Set("Cookie", res2.Header().Get("Set-Cookie"))
|
||||||
|
r.ServeHTTP(res3, req3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionFlashes(t *testing.T, newStore storeFactory) {
|
||||||
|
r := gin.Default()
|
||||||
|
store := newStore(t)
|
||||||
|
store.Options(Options{
|
||||||
|
Domain: "localhost",
|
||||||
|
})
|
||||||
|
r.Use(Sessions(sessionName, store))
|
||||||
|
|
||||||
|
r.GET("/set", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
session.AddFlash(ok)
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/flash", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
l := len(session.Flashes())
|
||||||
|
if l != 1 {
|
||||||
|
t.Error("Flashes count does not equal 1. Equals ", l)
|
||||||
|
}
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/check", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
l := len(session.Flashes())
|
||||||
|
if l != 0 {
|
||||||
|
t.Error("flashes count is not 0 after reading. Equals ", l)
|
||||||
|
}
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
res1 := httptest.NewRecorder()
|
||||||
|
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||||
|
r.ServeHTTP(res1, req1)
|
||||||
|
|
||||||
|
res2 := httptest.NewRecorder()
|
||||||
|
req2, _ := http.NewRequest("GET", "/flash", nil)
|
||||||
|
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||||
|
r.ServeHTTP(res2, req2)
|
||||||
|
|
||||||
|
res3 := httptest.NewRecorder()
|
||||||
|
req3, _ := http.NewRequest("GET", "/check", nil)
|
||||||
|
req3.Header.Set("Cookie", res2.Header().Get("Set-Cookie"))
|
||||||
|
r.ServeHTTP(res3, req3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionClear(t *testing.T, newStore storeFactory) {
|
||||||
|
data := map[string]string{
|
||||||
|
"key": "val",
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
r := gin.Default()
|
||||||
|
store := newStore(t)
|
||||||
|
r.Use(Sessions(sessionName, store))
|
||||||
|
|
||||||
|
r.GET("/set", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
for k, v := range data {
|
||||||
|
session.Set(k, v)
|
||||||
|
}
|
||||||
|
session.Clear()
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/check", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
for k, v := range data {
|
||||||
|
if session.Get(k) == v {
|
||||||
|
t.Fatal("Session clear failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
res1 := httptest.NewRecorder()
|
||||||
|
req1, _ := http.NewRequest("GET", "/set", nil)
|
||||||
|
r.ServeHTTP(res1, req1)
|
||||||
|
|
||||||
|
res2 := httptest.NewRecorder()
|
||||||
|
req2, _ := http.NewRequest("GET", "/check", nil)
|
||||||
|
req2.Header.Set("Cookie", res1.Header().Get("Set-Cookie"))
|
||||||
|
r.ServeHTTP(res2, req2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionOptions(t *testing.T, newStore storeFactory) {
|
||||||
|
r := gin.Default()
|
||||||
|
store := newStore(t)
|
||||||
|
store.Options(Options{
|
||||||
|
Domain: "localhost",
|
||||||
|
})
|
||||||
|
r.Use(Sessions(sessionName, store))
|
||||||
|
|
||||||
|
r.GET("/domain", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
session.Set("key", ok)
|
||||||
|
session.Options(Options{
|
||||||
|
Path: "/foo/bar/bat",
|
||||||
|
})
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
r.GET("/path", func(c *gin.Context) {
|
||||||
|
session := Default(c)
|
||||||
|
session.Set("key", ok)
|
||||||
|
session.Save()
|
||||||
|
c.String(200, ok)
|
||||||
|
})
|
||||||
|
res1 := httptest.NewRecorder()
|
||||||
|
req1, _ := http.NewRequest("GET", "/domain", nil)
|
||||||
|
r.ServeHTTP(res1, req1)
|
||||||
|
|
||||||
|
res2 := httptest.NewRecorder()
|
||||||
|
req2, _ := http.NewRequest("GET", "/path", nil)
|
||||||
|
r.ServeHTTP(res2, req2)
|
||||||
|
|
||||||
|
s := strings.Split(res1.Header().Get("Set-Cookie"), ";")
|
||||||
|
if s[1] != " Path=/foo/bar/bat" {
|
||||||
|
t.Error("Error writing path with options:", s[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.Split(res2.Header().Get("Set-Cookie"), ";")
|
||||||
|
if s[1] != " Domain=localhost" {
|
||||||
|
t.Error("Error writing domain with options:", s[1])
|
||||||
|
}
|
||||||
|
}
|
15
vendor/github.com/gin-contrib/sse/.travis.yml
generated
vendored
Normal file
15
vendor/github.com/gin-contrib/sse/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
go:
|
||||||
|
- 1.6.4
|
||||||
|
- 1.7.4
|
||||||
|
- tip
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 3
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -v -covermode=count -coverprofile=coverage.out
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
21
vendor/github.com/gin-contrib/sse/LICENSE
generated
vendored
Normal file
21
vendor/github.com/gin-contrib/sse/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Manuel Martínez-Almeida
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
58
vendor/github.com/gin-contrib/sse/README.md
generated
vendored
Normal file
58
vendor/github.com/gin-contrib/sse/README.md
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Server-Sent Events
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/gin-contrib/sse?status.svg)](https://godoc.org/github.com/gin-contrib/sse)
|
||||||
|
[![Build Status](https://travis-ci.org/gin-contrib/sse.svg)](https://travis-ci.org/gin-contrib/sse)
|
||||||
|
[![codecov](https://codecov.io/gh/gin-contrib/sse/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/sse)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/sse)](https://goreportcard.com/report/github.com/gin-contrib/sse)
|
||||||
|
|
||||||
|
Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. The Server-Sent Events EventSource API is [standardized as part of HTML5[1] by the W3C](http://www.w3.org/TR/2009/WD-eventsource-20091029/).
|
||||||
|
|
||||||
|
- [Read this great SSE introduction by the HTML5Rocks guys](http://www.html5rocks.com/en/tutorials/eventsource/basics/)
|
||||||
|
- [Browser support](http://caniuse.com/#feat=eventsource)
|
||||||
|
|
||||||
|
## Sample code
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/gin-contrib/sse"
|
||||||
|
|
||||||
|
func httpHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// data can be a primitive like a string, an integer or a float
|
||||||
|
sse.Encode(w, sse.Event{
|
||||||
|
Event: "message",
|
||||||
|
Data: "some data\nmore data",
|
||||||
|
})
|
||||||
|
|
||||||
|
// also a complex type, like a map, a struct or a slice
|
||||||
|
sse.Encode(w, sse.Event{
|
||||||
|
Id: "124",
|
||||||
|
Event: "message",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"user": "manu",
|
||||||
|
"date": time.Now().Unix(),
|
||||||
|
"content": "hi!",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
event: message
|
||||||
|
data: some data\\nmore data
|
||||||
|
|
||||||
|
id: 124
|
||||||
|
event: message
|
||||||
|
data: {"content":"hi!","date":1431540810,"user":"manu"}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content-Type
|
||||||
|
|
||||||
|
```go
|
||||||
|
fmt.Println(sse.ContentType)
|
||||||
|
```
|
||||||
|
```
|
||||||
|
text/event-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
## Decoding support
|
||||||
|
|
||||||
|
There is a client-side implementation of SSE coming soon.
|
116
vendor/github.com/gin-contrib/sse/sse-decoder.go
generated
vendored
Normal file
116
vendor/github.com/gin-contrib/sse/sse-decoder.go
generated
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type decoder struct {
|
||||||
|
events []Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(r io.Reader) ([]Event, error) {
|
||||||
|
var dec decoder
|
||||||
|
return dec.decode(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoder) dispatchEvent(event Event, data string) {
|
||||||
|
dataLength := len(data)
|
||||||
|
if dataLength > 0 {
|
||||||
|
//If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer.
|
||||||
|
data = data[:dataLength-1]
|
||||||
|
dataLength--
|
||||||
|
}
|
||||||
|
if dataLength == 0 && event.Event == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Event == "" {
|
||||||
|
event.Event = "message"
|
||||||
|
}
|
||||||
|
event.Data = data
|
||||||
|
d.events = append(d.events, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoder) decode(r io.Reader) ([]Event, error) {
|
||||||
|
buf, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentEvent Event
|
||||||
|
var dataBuffer *bytes.Buffer = new(bytes.Buffer)
|
||||||
|
// TODO (and unit tests)
|
||||||
|
// Lines must be separated by either a U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair,
|
||||||
|
// a single U+000A LINE FEED (LF) character,
|
||||||
|
// or a single U+000D CARRIAGE RETURN (CR) character.
|
||||||
|
lines := bytes.Split(buf, []byte{'\n'})
|
||||||
|
for _, line := range lines {
|
||||||
|
if len(line) == 0 {
|
||||||
|
// If the line is empty (a blank line). Dispatch the event.
|
||||||
|
d.dispatchEvent(currentEvent, dataBuffer.String())
|
||||||
|
|
||||||
|
// reset current event and data buffer
|
||||||
|
currentEvent = Event{}
|
||||||
|
dataBuffer.Reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line[0] == byte(':') {
|
||||||
|
// If the line starts with a U+003A COLON character (:), ignore the line.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var field, value []byte
|
||||||
|
colonIndex := bytes.IndexRune(line, ':')
|
||||||
|
if colonIndex != -1 {
|
||||||
|
// If the line contains a U+003A COLON character character (:)
|
||||||
|
// Collect the characters on the line before the first U+003A COLON character (:),
|
||||||
|
// and let field be that string.
|
||||||
|
field = line[:colonIndex]
|
||||||
|
// Collect the characters on the line after the first U+003A COLON character (:),
|
||||||
|
// and let value be that string.
|
||||||
|
value = line[colonIndex+1:]
|
||||||
|
// If value starts with a single U+0020 SPACE character, remove it from value.
|
||||||
|
if len(value) > 0 && value[0] == ' ' {
|
||||||
|
value = value[1:]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, the string is not empty but does not contain a U+003A COLON character character (:)
|
||||||
|
// Use the whole line as the field name, and the empty string as the field value.
|
||||||
|
field = line
|
||||||
|
value = []byte{}
|
||||||
|
}
|
||||||
|
// The steps to process the field given a field name and a field value depend on the field name,
|
||||||
|
// as given in the following list. Field names must be compared literally,
|
||||||
|
// with no case folding performed.
|
||||||
|
switch string(field) {
|
||||||
|
case "event":
|
||||||
|
// Set the event name buffer to field value.
|
||||||
|
currentEvent.Event = string(value)
|
||||||
|
case "id":
|
||||||
|
// Set the event stream's last event ID to the field value.
|
||||||
|
currentEvent.Id = string(value)
|
||||||
|
case "retry":
|
||||||
|
// If the field value consists of only characters in the range U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9),
|
||||||
|
// then interpret the field value as an integer in base ten, and set the event stream's reconnection time to that integer.
|
||||||
|
// Otherwise, ignore the field.
|
||||||
|
currentEvent.Id = string(value)
|
||||||
|
case "data":
|
||||||
|
// Append the field value to the data buffer,
|
||||||
|
dataBuffer.Write(value)
|
||||||
|
// then append a single U+000A LINE FEED (LF) character to the data buffer.
|
||||||
|
dataBuffer.WriteString("\n")
|
||||||
|
default:
|
||||||
|
//Otherwise. The field is ignored.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Once the end of the file is reached, the user agent must dispatch the event one final time.
|
||||||
|
d.dispatchEvent(currentEvent, dataBuffer.String())
|
||||||
|
|
||||||
|
return d.events, nil
|
||||||
|
}
|
116
vendor/github.com/gin-contrib/sse/sse-decoder_test.go
generated
vendored
Normal file
116
vendor/github.com/gin-contrib/sse/sse-decoder_test.go
generated
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeSingle1(t *testing.T) {
|
||||||
|
events, err := Decode(bytes.NewBufferString(
|
||||||
|
`data: this is a text
|
||||||
|
event: message
|
||||||
|
fake:
|
||||||
|
id: 123456789010
|
||||||
|
: we can append data
|
||||||
|
: and multiple comments should not break it
|
||||||
|
data: a very nice one`))
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, events, 1)
|
||||||
|
assert.Equal(t, events[0].Event, "message")
|
||||||
|
assert.Equal(t, events[0].Id, "123456789010")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeSingle2(t *testing.T) {
|
||||||
|
events, err := Decode(bytes.NewBufferString(
|
||||||
|
`: starting with a comment
|
||||||
|
fake:
|
||||||
|
|
||||||
|
data:this is a \ntext
|
||||||
|
event:a message\n\n
|
||||||
|
fake
|
||||||
|
:and multiple comments\n should not break it\n\n
|
||||||
|
id:1234567890\n10
|
||||||
|
:we can append data
|
||||||
|
data:a very nice one\n!
|
||||||
|
|
||||||
|
|
||||||
|
`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, events, 1)
|
||||||
|
assert.Equal(t, events[0].Event, "a message\\n\\n")
|
||||||
|
assert.Equal(t, events[0].Id, "1234567890\\n10")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeSingle3(t *testing.T) {
|
||||||
|
events, err := Decode(bytes.NewBufferString(
|
||||||
|
`
|
||||||
|
id:123456ABCabc789010
|
||||||
|
event: message123
|
||||||
|
: we can append data
|
||||||
|
data:this is a text
|
||||||
|
data: a very nice one
|
||||||
|
data:
|
||||||
|
data
|
||||||
|
: ending with a comment`))
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, events, 1)
|
||||||
|
assert.Equal(t, events[0].Event, "message123")
|
||||||
|
assert.Equal(t, events[0].Id, "123456ABCabc789010")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeMulti1(t *testing.T) {
|
||||||
|
events, err := Decode(bytes.NewBufferString(
|
||||||
|
`
|
||||||
|
id:
|
||||||
|
event: weird event
|
||||||
|
data:this is a text
|
||||||
|
:data: this should NOT APER
|
||||||
|
data: second line
|
||||||
|
|
||||||
|
: a comment
|
||||||
|
event: message
|
||||||
|
id:123
|
||||||
|
data:this is a text
|
||||||
|
:data: this should NOT APER
|
||||||
|
data: second line
|
||||||
|
|
||||||
|
|
||||||
|
: a comment
|
||||||
|
event: message
|
||||||
|
id:123
|
||||||
|
data:this is a text
|
||||||
|
data: second line
|
||||||
|
|
||||||
|
:hola
|
||||||
|
|
||||||
|
data
|
||||||
|
|
||||||
|
event:
|
||||||
|
|
||||||
|
id`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, events, 3)
|
||||||
|
assert.Equal(t, events[0].Event, "weird event")
|
||||||
|
assert.Equal(t, events[0].Id, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeW3C(t *testing.T) {
|
||||||
|
events, err := Decode(bytes.NewBufferString(
|
||||||
|
`data
|
||||||
|
|
||||||
|
data
|
||||||
|
data
|
||||||
|
|
||||||
|
data:
|
||||||
|
`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, events, 1)
|
||||||
|
}
|
110
vendor/github.com/gin-contrib/sse/sse-encoder.go
generated
vendored
Normal file
110
vendor/github.com/gin-contrib/sse/sse-encoder.go
generated
vendored
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server-Sent Events
|
||||||
|
// W3C Working Draft 29 October 2009
|
||||||
|
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
|
||||||
|
|
||||||
|
const ContentType = "text/event-stream"
|
||||||
|
|
||||||
|
var contentType = []string{ContentType}
|
||||||
|
var noCache = []string{"no-cache"}
|
||||||
|
|
||||||
|
var fieldReplacer = strings.NewReplacer(
|
||||||
|
"\n", "\\n",
|
||||||
|
"\r", "\\r")
|
||||||
|
|
||||||
|
var dataReplacer = strings.NewReplacer(
|
||||||
|
"\n", "\ndata:",
|
||||||
|
"\r", "\\r")
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Event string
|
||||||
|
Id string
|
||||||
|
Retry uint
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Encode(writer io.Writer, event Event) error {
|
||||||
|
w := checkWriter(writer)
|
||||||
|
writeId(w, event.Id)
|
||||||
|
writeEvent(w, event.Event)
|
||||||
|
writeRetry(w, event.Retry)
|
||||||
|
return writeData(w, event.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeId(w stringWriter, id string) {
|
||||||
|
if len(id) > 0 {
|
||||||
|
w.WriteString("id:")
|
||||||
|
fieldReplacer.WriteString(w, id)
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEvent(w stringWriter, event string) {
|
||||||
|
if len(event) > 0 {
|
||||||
|
w.WriteString("event:")
|
||||||
|
fieldReplacer.WriteString(w, event)
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRetry(w stringWriter, retry uint) {
|
||||||
|
if retry > 0 {
|
||||||
|
w.WriteString("retry:")
|
||||||
|
w.WriteString(strconv.FormatUint(uint64(retry), 10))
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeData(w stringWriter, data interface{}) error {
|
||||||
|
w.WriteString("data:")
|
||||||
|
switch kindOfData(data) {
|
||||||
|
case reflect.Struct, reflect.Slice, reflect.Map:
|
||||||
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.WriteString("\n")
|
||||||
|
default:
|
||||||
|
dataReplacer.WriteString(w, fmt.Sprint(data))
|
||||||
|
w.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Event) Render(w http.ResponseWriter) error {
|
||||||
|
r.WriteContentType(w)
|
||||||
|
return Encode(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Event) WriteContentType(w http.ResponseWriter) {
|
||||||
|
header := w.Header()
|
||||||
|
header["Content-Type"] = contentType
|
||||||
|
|
||||||
|
if _, exist := header["Cache-Control"]; !exist {
|
||||||
|
header["Cache-Control"] = noCache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func kindOfData(data interface{}) reflect.Kind {
|
||||||
|
value := reflect.ValueOf(data)
|
||||||
|
valueType := value.Kind()
|
||||||
|
if valueType == reflect.Ptr {
|
||||||
|
valueType = value.Elem().Kind()
|
||||||
|
}
|
||||||
|
return valueType
|
||||||
|
}
|
255
vendor/github.com/gin-contrib/sse/sse_test.go
generated
vendored
Normal file
255
vendor/github.com/gin-contrib/sse/sse_test.go
generated
vendored
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeOnlyData(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
event := Event{
|
||||||
|
Data: "junk\n\njk\nid:fake",
|
||||||
|
}
|
||||||
|
err := Encode(w, event)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(),
|
||||||
|
`data:junk
|
||||||
|
data:
|
||||||
|
data:jk
|
||||||
|
data:id:fake
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
decoded, _ := Decode(w)
|
||||||
|
assert.Equal(t, decoded, []Event{event})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeWithEvent(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
event := Event{
|
||||||
|
Event: "t\n:<>\r\test",
|
||||||
|
Data: "junk\n\njk\nid:fake",
|
||||||
|
}
|
||||||
|
err := Encode(w, event)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(),
|
||||||
|
`event:t\n:<>\r est
|
||||||
|
data:junk
|
||||||
|
data:
|
||||||
|
data:jk
|
||||||
|
data:id:fake
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
decoded, _ := Decode(w)
|
||||||
|
assert.Equal(t, decoded, []Event{event})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeWithId(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Id: "t\n:<>\r\test",
|
||||||
|
Data: "junk\n\njk\nid:fa\rke",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(),
|
||||||
|
`id:t\n:<>\r est
|
||||||
|
data:junk
|
||||||
|
data:
|
||||||
|
data:jk
|
||||||
|
data:id:fa\rke
|
||||||
|
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeWithRetry(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Retry: 11,
|
||||||
|
Data: "junk\n\njk\nid:fake\n",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(),
|
||||||
|
`retry:11
|
||||||
|
data:junk
|
||||||
|
data:
|
||||||
|
data:jk
|
||||||
|
data:id:fake
|
||||||
|
data:
|
||||||
|
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeWithEverything(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Event: "abc",
|
||||||
|
Id: "12345",
|
||||||
|
Retry: 10,
|
||||||
|
Data: "some data",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "id:12345\nevent:abc\nretry:10\ndata:some data\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeMap(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Event: "a map",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"foo": "b\n\rar",
|
||||||
|
"bar": "id: 2",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "event:a map\ndata:{\"bar\":\"id: 2\",\"foo\":\"b\\n\\rar\"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSlice(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Event: "a slice",
|
||||||
|
Data: []interface{}{1, "text", map[string]interface{}{"foo": "bar"}},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "event:a slice\ndata:[1,\"text\",{\"foo\":\"bar\"}]\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeStruct(t *testing.T) {
|
||||||
|
myStruct := struct {
|
||||||
|
A int
|
||||||
|
B string `json:"value"`
|
||||||
|
}{1, "number"}
|
||||||
|
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Event: "a struct",
|
||||||
|
Data: myStruct,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "event:a struct\ndata:{\"A\":1,\"value\":\"number\"}\n\n")
|
||||||
|
|
||||||
|
w.Reset()
|
||||||
|
err = Encode(w, Event{
|
||||||
|
Event: "a struct",
|
||||||
|
Data: &myStruct,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "event:a struct\ndata:{\"A\":1,\"value\":\"number\"}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeInteger(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Event: "an integer",
|
||||||
|
Data: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "event:an integer\ndata:1\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeFloat(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := Encode(w, Event{
|
||||||
|
Event: "Float",
|
||||||
|
Data: 1.5,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.String(), "event:Float\ndata:1.5\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeStream(t *testing.T) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
|
||||||
|
Encode(w, Event{
|
||||||
|
Event: "float",
|
||||||
|
Data: 1.5,
|
||||||
|
})
|
||||||
|
|
||||||
|
Encode(w, Event{
|
||||||
|
Id: "123",
|
||||||
|
Data: map[string]interface{}{"foo": "bar", "bar": "foo"},
|
||||||
|
})
|
||||||
|
|
||||||
|
Encode(w, Event{
|
||||||
|
Id: "124",
|
||||||
|
Event: "chat",
|
||||||
|
Data: "hi! dude",
|
||||||
|
})
|
||||||
|
assert.Equal(t, w.String(), "event:float\ndata:1.5\n\nid:123\ndata:{\"bar\":\"foo\",\"foo\":\"bar\"}\n\nid:124\nevent:chat\ndata:hi! dude\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderSSE(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
err := (Event{
|
||||||
|
Event: "msg",
|
||||||
|
Data: "hi! how are you?",
|
||||||
|
}).Render(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "event:msg\ndata:hi! how are you?\n\n")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "text/event-stream")
|
||||||
|
assert.Equal(t, w.Header().Get("Cache-Control"), "no-cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkResponseWriter(b *testing.B) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
(Event{
|
||||||
|
Event: "new_message",
|
||||||
|
Data: "hi! how are you? I am fine. this is a long stupid message!!!",
|
||||||
|
}).Render(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFullSSE(b *testing.B) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Encode(buf, Event{
|
||||||
|
Event: "new_message",
|
||||||
|
Id: "13435",
|
||||||
|
Retry: 10,
|
||||||
|
Data: "hi! how are you? I am fine. this is a long stupid message!!!",
|
||||||
|
})
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNoRetrySSE(b *testing.B) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Encode(buf, Event{
|
||||||
|
Event: "new_message",
|
||||||
|
Id: "13435",
|
||||||
|
Data: "hi! how are you? I am fine. this is a long stupid message!!!",
|
||||||
|
})
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSimpleSSE(b *testing.B) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Encode(buf, Event{
|
||||||
|
Event: "new_message",
|
||||||
|
Data: "hi! how are you? I am fine. this is a long stupid message!!!",
|
||||||
|
})
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
}
|
24
vendor/github.com/gin-contrib/sse/writer.go
generated
vendored
Normal file
24
vendor/github.com/gin-contrib/sse/writer.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package sse
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type stringWriter interface {
|
||||||
|
io.Writer
|
||||||
|
WriteString(string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringWrapper struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w stringWrapper) WriteString(str string) (int, error) {
|
||||||
|
return w.Writer.Write([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkWriter(writer io.Writer) stringWriter {
|
||||||
|
if w, ok := writer.(stringWriter); ok {
|
||||||
|
return w
|
||||||
|
} else {
|
||||||
|
return stringWrapper{writer}
|
||||||
|
}
|
||||||
|
}
|
4
vendor/github.com/gin-gonic/gin/.gitignore
generated
vendored
Normal file
4
vendor/github.com/gin-gonic/gin/.gitignore
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
vendor/*
|
||||||
|
!vendor/vendor.json
|
||||||
|
coverage.out
|
||||||
|
count.out
|
31
vendor/github.com/gin-gonic/gin/.travis.yml
generated
vendored
Normal file
31
vendor/github.com/gin-gonic/gin/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
go:
|
||||||
|
- 1.6.x
|
||||||
|
- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
- master
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 3
|
||||||
|
|
||||||
|
install:
|
||||||
|
- make install
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make vet
|
||||||
|
- make fmt-check
|
||||||
|
- make embedmd
|
||||||
|
- make misspell-check
|
||||||
|
- make test
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks:
|
||||||
|
urls:
|
||||||
|
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
|
||||||
|
on_success: change # options: [always|never|change] default: always
|
||||||
|
on_failure: always # options: [always|never|change] default: always
|
||||||
|
on_start: false # default: false
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user