Compare commits

..

39 Commits

Author SHA1 Message Date
deepzz0
54f5289d6b release v1.20 2017-06-14 21:25:18 +08:00
deepzz0
1634418a13 fix disqus bug 2017-06-12 01:44:28 +08:00
deepzz0
a84a474504 Merge branch 'master' of github.com:eiblog/eiblog 2017-06-12 00:20:10 +08:00
deepzz0
b64cf5985a fix disqus bug 2017-06-12 00:19:17 +08:00
Deepzz
4f9965b6bd Update README.md 2017-05-26 08:59:57 +08:00
Deepzz
daffa6c294 Update CHANGELOG.md 2017-05-12 22:07:05 +08:00
Deepzz
0bd738438e Update install.md 2017-05-12 22:03:19 +08:00
Deepzz
309339492c Update README.md 2017-05-05 18:10:43 +08:00
Deepzz
694036c65f Update README.md 2017-05-05 18:08:36 +08:00
Deepzz
cafdaac9f4 Update README.md 2017-05-05 18:07:20 +08:00
henry.chen
f6956f592f update 2017-04-28 18:01:19 +08:00
deepzz0
2382f76cf6 merge 2017-04-22 10:50:06 +08:00
deepzz0
a66a3c0198 update disqus.js->disqus_921d24.js 2017-04-22 10:47:01 +08:00
henry.chen
31c398700e 将配置分离出来 2017-04-03 10:43:40 +08:00
henry.chen
9296147a0f update 2017-04-03 01:18:46 +08:00
henry.chen
6932799cba update disqus_78bca4.js 2017-04-01 15:53:29 +08:00
Deepzz
b1ff8b59af Update README.md 2017-03-18 15:42:18 +08:00
henry.chen
5f047c2c27 update vendor directory 2017-03-11 00:54:27 +08:00
Deepzz
5d24af11e5 Update front.go 2017-03-10 09:27:34 +08:00
deepzz0
d03f327fb4 update 2017-03-08 20:45:47 +08:00
deepzz0
ef9e64469b use go1.8 2017-03-08 20:18:13 +08:00
deepzz0
86e7374997 update 2017-03-02 23:08:22 +08:00
deepzz0
4f24b80107 fix out of range about MapArticles 2017-03-02 23:07:36 +08:00
deepzz0
2c49a1ec8d Merge branch 'master' of github.com:eiblog/eiblog 2017-03-02 22:48:03 +08:00
deepzz0
3eaab0fb1f fix out of range about MapArticels 2017-03-02 22:47:44 +08:00
Deepzz
8e6404a90a Update writing.md 2017-02-25 22:47:53 +08:00
deepzz0
7775ea35a2 update docs 2017-02-25 22:42:28 +08:00
deepzz0
931d7b0683 document filing 2017-02-25 19:35:23 +08:00
deepzz0
562f4d86c6 add vendor 2017-02-18 15:23:57 +08:00
deepzz0
4ebbc38cc0 pretty *.xml 2017-02-17 23:53:07 +08:00
deepzz0
9509cd66e6 delete static/*.xml 2017-02-17 23:36:56 +08:00
deepzz0
48756a2810 generate by xml.go 2017-02-17 23:35:51 +08:00
deepzz0
ec8297c3f6 fix article desc error 2017-02-11 19:37:51 +08:00
deepzz0
d622a8397f update 2017-02-07 20:16:51 +08:00
deepzz0
c75619785d Merge branch 'master' of github.com:eiblog/eiblog 2017-02-02 10:49:22 +08:00
deepzz0
c014c6450b replace jquery 2017-02-02 10:48:43 +08:00
Deepzz
d8879f8d32 Update README.md 2017-01-17 21:33:52 +08:00
deepzz0
9bb0905aab simple article description 2017-01-11 01:19:59 +08:00
deepzz0
c39844ca63 update .travis.yml 2017-01-09 23:28:21 +08:00
1419 changed files with 378275 additions and 430 deletions

View File

@@ -4,6 +4,7 @@ setting
conf
static
views
docs
!static/tzdata
Dockerfile
glide.yaml

7
.gitignore vendored
View File

@@ -1,10 +1,5 @@
*.DS_Store
*.exe
vendor
vendor/**
conf/ssl/domain.*
eiblog
static/feed.xml
static/opensearch.xml
static/sitemap.xml
static/*.*

View File

@@ -5,16 +5,16 @@ dist: trusty # 在ubuntu:trusty
language: go # 声明构建语言环境
go: # 只构建最新版本
- tip
- 1.8
services: # docker环境
- docker
# branches: # 限定项目分支
# only:
# - master
branches: # 限定项目分支
only:
- /^v[0-9](\.[0-9]){2}(-rc[1-9])?$/
before_install:
install:
- curl https://glide.sh/get | sh # 安装glide包管理
script:
@@ -23,10 +23,11 @@ script:
- docker build -t registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog . # 构建镜像
after_success:
- if [ "$TRAVIS_BRANCH" =~ ^v[0-9](\.[0-9])+.*$ ]; then
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" registry.cn-hangzhou.aliyuncs.com;
docker push registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog;
fi
# - if [ "$TRAVIS_BRANCH" =~ ^v[0-9](\.[0-9])+.*$ ]; then
# docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" registry.cn-hangzhou.aliyuncs.com;
# docker push registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog;
# fi
- docker push registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
before_deploy:
- ./dist.sh

View File

@@ -1,5 +1,27 @@
# Eiblog Changelog
## v1.2.0 (2017-06-14)
* 更新评论功能,基础评论 0 回复也可评论了。
* disqus.js 文件由博主自行更新。
* 更正描述 README.md 描述错误 [#4f996](https://github.com/eiblog/eiblog/commit/4f9965b6bdefe087dd0805c1840afcb2752cd155)。
* docker 镜像版本化。
## v1.1.3 (2017-05-12)
* 更新 disqus_78bca4.js 到 disqus_921d24.js具体请参考 docs/install.md
* 更新 vendor
## v1.1.2 (2017-03-08)
* 解决添加文章描述错误的bug
* 添加vendor目录
* 添加文档docs目录
* 删除多余注释
## v1.1.1 (2017-02-07)
* 添加文章描述功能。
* 修复评论`jQuery`文件引用错误。
* 修复`.travis.yml`描述错误。
## v1.0.0 (2016-01-09)
首次发布版本

303
README.md
View File

@@ -1,4 +1,4 @@
# EiBlog [![Build Status](https://travis-ci.org/eiblog/eiblog.svg?branch=master)](https://travis-ci.org/eiblog/eiblog)
# EiBlog [![Build Status](https://travis-ci.org/eiblog/eiblog.svg?branch=master)](https://travis-ci.org/eiblog/eiblog) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) [![Versuib](https://img.shields.io/github/tag/eiblog/eiblog.svg)](https://github.com/eiblog/eiblog/releases)
> 系统根据[https://imququ.com](https://imququ.com)一系列文章和方向进行搭建期间获得了QuQu的很大帮助在此表示感谢。
@@ -7,6 +7,7 @@
<!--more-->
### 介绍
整个博客系统涉及到模块如下:
* `MongoDB`,博客采用 mongodb 作为存储数据库。
@@ -16,40 +17,7 @@
* `Google Analytics`,作为博客系统的数据分析统计工具。
* `七牛 CDN`,作为博客系统的静态文件存储,博文的图片附件什么上传至这里。
相关技术有:
* `Golang`博客系统后端采用golang编写并开源至[Eiblog](https://github.com/eiblog/eiblog)。
* `HTML Javascript CSS`,博客系统的前端采用`html``jquery`编写,样式采用`CSS`
* `Glide` golang 编写。作为博客系统的包依赖管理器,其开源地址是[Glide](https://github.com/Masterminds/glide)。
* `Docker`,博客系统可 docker 部署,方便,快捷。
* `Docker Compose`,博客系统可完全 docker 运行compose起到很好管理作用。
* `SSL 证书``https`是未来的趋势,整个博客系统都将围绕着`证书`进行,请事先准备好一张有效的 ssl 证书。
* `Travis`作为博客系统的自动构建工具自动构建docker镜像并推送到镜像仓库。
* `Yaml`,博客系统的配置文件使用`yaml`,请悉知。
作为博主之心血之作,`Eiblog`实现了什么功能,有什么特点,做了什么优化呢?
1. 系统目前只有`首页``专题``归档``友链``关于``搜索`界面。相信已经可以满足大部分用户的需求。
2. `.js``.css`等静态文件本地存储,小图片 base64 内置到 css 中,不会产生网络所带来的延迟,加速网页访问。版本控制方式,动态更新静态文件。
3. 采用谷歌统计,并实现异步(将访问信息发给后端,后端提交给谷歌)统计,加速访问速度。
4. 采用直接缓存 markdown 转过的 html 文档的方式,加速后端处理。响应速度均在 3ms 以内,真正极速。
5. 通过 Nginx 的配置,开启压缩缩小传输量,服务器传输证书链、开启`Session Resumption``Session Ticket``OCSP Stapling`等加速证书握手,再次提高速度。
6. 文章评论数量(不重要)后端跑定时脚本,定时更新,所以有时评论数是不对的。这样减少了 api 调用,又再次达到加速访问的目的。
7. 针对`disqus`被墙原因,实现[Jerry Qu](https://imququ.com)的另类评论方式,保证评论的流畅。
8. 开源`Typecho`完整后台系统,全功能`markdown`编辑器,让你体验什么是简洁清爽。
9. 博客后台直接对接`七牛 SDK`,实现后台上传文件和删除文件的简单功能。
10. 采用`elasticsearch`作为站内搜索,添加`google opensearch`功能,搜索更加自然。
当然,在信息安全方面也没少下功夫,虽然我们只是一个小小的博客系统。
1. `CDN`使用七牛融合CDN`https`化,实现全站`https`。七牛可申请免费证书了。
2. `CT`,证书透明度检测,提供一个开放的审计和监控系统。可以让任何域名所有者或者 CA 确定证书是否被错误签发或者被恶意使用,从而提高 HTTPS 网站的安全性。
3. `OSCP`,在线证书状态协议。用来检验证书合法性的在线查询服务.
4. `HSTS`,强制客户端(如浏览器)使用 HTTPS 与服务器创建连接。可以很好的解决 HTTPS 降级攻击。
5. `HPKP`HTTP公钥固定扩展防范由「伪造或不正当手段获得网站证书」造成的中间人攻击。该功能让我们选择信任哪些`CA`
5. `SSL Protocols`,罗列支持的`TLS`协议SSLv3被证实是不安全的。
6. `SSL dhparam`,迪菲赫尔曼密钥交换。
7. `Cipher suite`,罗列服务器支持加密套件。
### 图片展示
可以容易的看到[httpsecurityreport](https://httpsecurityreport.com/?report=deepzz.com)评分`96`[ssllabs](https://www.ssllabs.com/ssltest/analyze.html?d=deepzz.com&latest)评分`A+`,堪称完美。这些安全的相关配置会在后面的部署过程中接触到。
@@ -60,246 +28,55 @@
![show-admin](http://7xokm2.com1.z0.glb.clouddn.com/static/img/show-admin.png)
![show-time](http://7xokm2.com1.z0.glb.clouddn.com/static/img/eiblog-time.png)
![eiblog-mem](http://7xokm2.com1.z0.glb.clouddn.com/img/eiblog-mem.png)
> `注`图片1图片2是博客界面图片3是后台界面图片4是性能展示。
好了,说了那么多,吹了那么多,我们实际来动手搭建一个`Eiblog`吧。
### 极速体验
1. 到[这里](https://github.com/eiblog/eiblog/releases)下载对应平台`.tar.gz`文件。
### 安装
1、`Eiblog`提供多个平台的压缩包下载,可到[Eiblog release](https://github.com/eiblog/eiblog/releases)选择相应版本和平台下载。也可通过:
``` sh
$ curl -L https://github.com/eiblog/eiblog/releases/download/v1.0.0/eiblog-v1.0.0.`uname -s | tr '[A-Z]' '[a-z]'`-amd64.tar.gz > eiblog-v1.0.0.`uname -s | tr '[A-Z]' '[a-z]'`-amd64.tar.gz
2. 搭建`MongoDB`(必须)和`Elasticsearch`(可选)服务。
3. 修改`/etc/hosts`文件,添加`MongoDB`数据库 IP 地址,如:`127.0.0.1 mongodb`
4. 执行`./eiblog`,运行博客系统。看到:
```
2、如果有幸你也是`Gopher`,相信你会亲自动手,你可以通过:
``` sh
$ go get https://github.com/eiblog/eiblog
...
...
[GIN-debug] Listening and serving HTTP on :9000
```
进行源码编译二进制文件运行
代表运行成功了
3、如果你对`docker`技术也有研究的话,你也可以通过`docker`来安装:
``` sh
$ docker pull registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
默认监听`9000`端口,后台`/admin/login`,默认账号密码均为`deepzz`。更多详细请查阅[安装部署](https://github.com/eiblog/eiblog/blob/master/docs/install.md)文档。
```
镜像内部只提供了`eiblog`的二进制文件,因为其它内容定制化的需求过高。所以需要将`conf`、`static`、`views`目录映射出来,后面会具体说到。
### 特色功能
### 本地测试
在我们下载好可执行程序之后,我们可以开始本地测试的工作了。
作为博主之心血之作,`Eiblog`实现了什么功能,有什么特点,做了什么优化呢?
本地测试需要搭建两个服务`mongodb`和`elasticsearch2.4.1`(可选,搜索服务不可用)
1. 系统目前只有`首页``专题``归档``友链``关于``搜索`界面。相信已经可以满足大部分用户的需求
2. `.js``.css`等静态文件本地存储,小图片 base64 内置到 css 中,不会产生网络所带来的延迟,加速网页访问。版本控制方式,动态更新静态文件。
3. 采用谷歌统计,并实现异步(将访问信息发给后端,后端提交给谷歌)统计,加速访问速度。
4. 采用直接缓存 markdown 转过的 html 文档的方式,加速后端处理。响应速度均在 3ms 以内,真正极速。
5. 通过 Nginx 的配置,开启压缩缩小传输量,服务器传输证书链、开启`Session Resumption``Session Ticket``OCSP Stapling`等加速证书握手,再次提高速度。
* `CDN`使用七牛融合CDN`https`化,实现全站`https`。七牛可申请免费证书了。
* `CT`,证书透明度检测,提供一个开放的审计和监控系统。可以让任何域名所有者或者 CA 确定证书是否被错误签发或者被恶意使用,从而提高 HTTPS 网站的安全性。
* `OSCP`,在线证书状态协议。用来检验证书合法性的在线查询服务.
* `HSTS`,强制客户端(如浏览器)使用 HTTPS 与服务器创建连接。可以很好的解决 HTTPS 降级攻击。
* `HPKP`HTTP公钥固定扩展防范由「伪造或不正当手段获得网站证书」造成的中间人攻击。该功能让我们选择信任哪些`CA`
* `SSL Protocols`,罗列支持的`TLS`协议SSLv3被证实是不安全的。
* `SSL dhparam`,迪菲赫尔曼密钥交换。
* `Cipher suite`,罗列服务器支持加密套件。
6. 文章评论数量(不重要)后端跑定时脚本,定时更新,所以有时评论数是不对的。这样减少了 api 调用,又再次达到加速访问的目的。
7. 针对`disqus`被墙原因,实现[Jerry Qu](https://imququ.com)的另类评论方式,保证评论的流畅。
8. 开源`Typecho`完整后台系统,全功能`markdown`编辑器,让你体验什么是简洁清爽。
9. 博客后台直接对接`七牛 SDK`,实现后台上传文件和删除文件的简单功能。
10. 采用`elasticsearch`作为站内搜索,添加`google opensearch`功能,搜索更加自然。
`Eiblog`默认会连接`hostname`为`eidb`和`eisearch`,因此你需要将信息填入`/etc/hosts`下。假如你搭建的`mongodb`地址为`127.0.0.1:27017``elasticsearch`地址为`192.168.99.100:9200`,如:
``` sh
$ sudo vi /etc/hosts
# 在末尾加上两行
127.0.0.1 eidb
192.168.99.100 eisearch
```
#### MongoDB 搭建
1、`MongoDB`搭建Mac 可通过`brew install mongo`进行安装,其它平台请查询资料。
#### Elasticsearch 搭建
2、`Elasticsearch`搭建,它的搭建要些许复杂。博主尚未接触如何直接安装,因此建议通过`docker`搭建。需要注意的是 es 自带的分析器对中文分词是不友好的,这里采用了`elasticsearch-analysis-ik`分词器。如果你想了解更多[Github](https://github.com/medcl/elasticsearch-analysis-ik)或则如何实现[博客站内搜索](https://imququ.com/post/elasticsearch.html)。
* pull 镜像`docker pull elasticsearch:2.4.1`,必需使用该版本。
* 添加环境变量`ES_JAVA_OPTS: "-Xms512m -Xmx512m"`,除非你想让你的服务器爆掉。
* 映射相关目录:
```
conf/es/config:/usr/share/elasticsearch/config
conf/es/plugins:/usr/share/elasticsearch/plugins
conf/es/data:/usr/share/elasticsearch/data
conf/es/logs:/usr/share/elasticsearch/logs
```
 请将这四个目录映射至`eiblog`下的`conf`目录。如果你想查看更多,请查看`docker-compose.yml`文件。
总结一下,`docker`运行 es 的命令为:
``` sh
$ docker run -d --name eisearch \
-p 9200:9200 \
-e ES_JAVA_OPTS: "-Xms512m -Xmx512m" \
-v conf/es/config:/usr/share/elasticsearch/config \
-v conf/es/plugins:/usr/share/elasticsearch/plugins \
-v conf/es/data:/usr/share/elasticsearch/data \
-v conf/es/logs:/usr/share/elasticsearch/logs \
elasticsearch:2.4.1
```
之后执行`./eiblog`,咱们的`eiblog`就可以运行起来了。
通过`127.0.0.1:9000`可以进入博客首页,`127.0.0.1:9000/admin/login`进入后台登陆,账号密码为`eiblog/conf/app.yml`下的`username`和`password`。也就是初始账号密码`deepz`、`deepzz`。
> `注意`,因为配置`conf/app.yml`均是博主自用配置。有些操作可能(如评论)会评论到我的博客,还请尽量避免,谢谢。
### 准备部署
如果你在感受了该博客的魅力了之后,仍然坚持想要搭建它。那么,恭喜你,获得的一款不想再更换的博客系统。下面,我们跟随步骤对部署流程进一步说明。
这里只提供`Docker`的相关部署说明。你如果需要其它方式部署,请参考该方式。
#### 前提准备
这里需要准备一些必要的东西,如果你已准备好。请跳过。
* `一台服务器`。
* `一个域名`,国内服务器需备案。
* `有效的证书`。一般使用免费的就可以。如:`Lets Encrypt`,另外`qcloud`、`七牛`也提供了免费证书的申请,均是全球可信。
* `七牛CDN`。博客只设计接入了七牛cdn相信该CDN服务商不会让你失望。
* `Disqus`。作为博客评论系统,你得有翻墙的能力注册到该账号,具体配置我想又可以写一片博客了。简单说需要`shorname`和`public key`。
* `Google Analystic`。数据统计分析工具。
* `Superfeedr`。加速 RSS 订阅。
* `Twitter`。希望你能够有一个twitter账号。
是不是这么多要求,很费解。其实当初该博客系统只是为个人而设计的,是自己心中想要的那一款。博主些这篇文章不是想要多少人来用该博客,而是希望对那些追求至极的朋友说:你需要这款博客系统。
#### 文件准备
尽管大多数文件已经准备好。但有些默认的文件需要特别指出来,需要你在 CDN 上写特殊的路径。
假如你的 CDN 域名为`st.example.com`,那么:
* `favicon.ico`,其 URL 应该是`st.example.com/static/img/favicon.ico`。故你在 CDN 中的文件名为`static/img/favicon.ico`,以下如是。
* `左侧背景图片``500*1200`左右CDN 中文件名:`static/img/bg04.jpg`。如需更改,请在`eiblog/view/st_blog.css`中替换该名称。
* `头像``160*160~256*256`之间CDN 文件名:`static/img/avatar.jpg`。另外你需要将该图片 `Base64` 编码后替换掉`eiblog/views/st_blog.css`中合适位置的图片。
* `blank.gif`CDN 文件名:`static/img/blank.gif`。该图片请从[这里](https://st.deepzz.com/static/img/blank.gif)下载并上传至你的 CDN。
* `default_avatar.png`CDN 文件名:`static/img/default_avatar.png`,请从[这里](https://st.deepzz.com/static/img/default_avatar.png)下载并上传至你的 CDN。
* `disqus.js`,该文件名是会变的,每次更新如果没有提及就没有改变,更新说明在[这里](https://github.com/eiblog/eiblog/blob/master/CHANGELOG.md)。CDN 文件名格式是:`static/js/name.js`。在我写这篇文章是使用的是:`static/js/disqus_a9d3fd.js`,请从[这里](https://st.deepzz.com/static/js/disqus_a9d3fd.js)下载并上传至你的 CDN。
> `注意`:本人 CDN 做了防盗链处理,故请将这些资源上传至您的 CDN ,以免静态资源不能访问,请悉知。
#### 配置说明
走到这里,我相信只走到`60%`的路程。放弃还来得及。
这里会对`eiblog/conf`下的所有文件做说明,希望你做好准备。
```
├── app.yml # 博客配置文件
├── blackip.yml # 博客ip黑名单
├── es # elasticsearch配置
│   ├── config # 配置文件
│   │   ├── analysis # 同义词
│   │   ├── elasticsearch.yml # 具体配置
│   │   ├── logging.yml # 日志配置
│   │   └── scripts # 脚本文件夹
│   └── plugins # 插件文件夹
│   └── ik1.10.1 # ik分词器
├── nginx # nginx配置
│   ├── domain # 域名配置nginx会读区改文件夹下的.conf文件
│   │   └── deepzz.conf
│   ├── ip.blacklist # nginx ip黑名单
│   └── nginx.conf # nginx配置请替换原有配置
├── scts # ct文件
│   ├── aviator.sct
│   └── digicert.sct
├── ssl # 证书文件具体请看deepzz.conf
│   ├── dhparams.pem
│   ├── domain.key
│   ├── domain.pem
│   ├── full_chained.pem
│   └── session_ticket.key
└── tpl # 模版文件
├── feedTpl.xml
├── opensearchTpl.xml
└── sitemapTpl.xml
```
1、app.yml整个程序的配置文件里面已经列出了所有配置项的说明这里不再阐述。
2、blackip.yml如果没有使用`Nginx`,博客内置`ip`过滤系统。
3、`es`全名`elasticsearch`,非常强大的分布式搜索引擎,`github`用的就是它。里面的配置基本不用修改,但`es/analysis/synonym.txt`是同义词,你可以照着已有的随意增加。
```
├── es
│   ├── config
│   │   ├── analysis
│   │   │   └── synonym.txt #同义词配置
│   │   ├── elasticsearch.yml #分词器配置
│   │   ├── logging.yml #日志配置
│   │   └── scripts #脚本
│   └── plugins #中文分词插件
│   └── ik1.10.0
```
> `注意`scripts文件夹虽然是空的但必需存在不然elasticsearch报错。
4、`nginx`,系统采用`nginx`作为代理(相信博客系统也不会独占一台服务器~)。请使用`nginx.conf`替换原`nginx`的配置。博客系统的配置文件是`domain/deepzz.conf`,或则重命名(只要是满足`*.conf`)。`deepzz.conf`文件里面学问是最多的。或许你想一一弄懂,或许…。
> 注意本配置需要更新nginx到最新版openssl更新到1.0.2j,具体请到 Jerry Qu 的[本博客 Nginx 配置之完整篇](https://imququ.com/post/my-nginx-conf.html)查看,了解详情。
5、`scts`,存放 ct 文件。
6、`ssl`,这里存放了所有证书相关的内容。
```
├── dhparams.pem #参见eiblog/conf/nginx/domain/deepzz.conf
├── domain.key #证书私钥,一般颁发者处下载
├── domain.pem #证书链,一般从证书颁发者那可以直接下载到
├── full_chained.pem #参见eiblog/conf/nginx/domain/deepzz.conf
└── session_ticket.key #参见eiblog/conf/nginx/domain/deepzz.conf
```
7、`tpl`模版相关,不用修改。
### 开始部署
#### docker
请确定你已经完成了上面所说的所有步骤,在本地已经测试成功。服务器上`MognoDB`和`Elasticsearch`已经安装并已经运行成功。
首先,请将本地测试好的`conf``static``views`文件夹上传至服务器,建议存储到服务器`/data/eiblog`下。
``` sh
$ tree /data/eiblog -L 1
├── conf
├── static
├── views
```
然后,将镜像 PULL 到服务器本地。
``` sh
# PULL下Eiblog镜像
$ docker pull registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
```
最后,执行`docker run`命令,希望你能成功。
``` sh
$ docker run -d --name eiblog --restart=always \
--add-host disqus.com:23.235.33.134 \
--link eidb --link eisearch \
-p 9000:9000 \
-e GODEBUG=netdns=cgo \
-v /data/eiblog/logdata:/eiblog/logdata \
-v /data/eiblog/conf:/eiblog/conf \
-v /data/eiblog/static:/eiblog/static \
-v /data/eiblog/views:/eiblog/views \
registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
```
这里默认`MongDB`和`Elasticsearch`均为`docker`部署,且名称为`eidb``eisearch`。
#### nginx + docker
通过`Nginx+docker`部署,是博主推荐的方式。这里采用`Docker Compose`管理我们整个博客系统。
请确认你已经成功安装好`Nginx`、`docker`、`docker-compose`。Nginx 请一定参照 Jerry Qu 的[Nginx 配置完整篇](https://imququ.com/post/my-nginx-conf.html)。
首先,请将本地测试好的`conf``static``views``docker-compose.yml`文件夹和文件上传至服务器。前三个文件夹建议存储到服务器`/data/eiblog`下,`docker-compose.yml`存放在你使用方便的地方。
> 注意`conf/es/config/scripts`空文件夹是否存在
``` sh
$ tree /data/eiblog -L 1
├── conf
├── static
├── views
$ ls ~/
docker-compose.yml
```
然后,执行:
``` sh
$ cd ~
$ docker-compose up -d
```
等待些许时间,成功运行。
### 文档
* [安装部署](https://github.com/eiblog/eiblog/blob/master/docs/install.md)
* [写作需知](https://github.com/eiblog/eiblog/blob/master/docs/writing.md)
* [好玩的功能](https://github.com/eiblog/eiblog/blob/master/docs/amusing.md)
### 成功搭建者博客

View File

@@ -12,6 +12,8 @@ pagesize: 20
length: 400
# 截取预览标识
identifier: <!--more-->
# 文章描述前缀
description: "Desc:"
# 起始ID预留id不时之需, 不用管
startid: 11
# elasticsearch url
@@ -42,7 +44,7 @@ kodo:
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
# 运行模式
mode:
# you can fix certfile, keyfile, domain
# 默认开启HTTP
enablehttp: true
httpport: 9000
enablehttps: false
@@ -64,7 +66,8 @@ pingrpcs:
- http://blogsearch.google.com/ping/RPC2
- http://rpc.pingomatic.com/
# 以下数据初始化用,可在后台修改
# 以下数据项供初始化使用,仅首次运行有效。
# 若需要修改,请到博客后台操作。
account:
# *后台登录用户名
username: deepzz
@@ -85,5 +88,5 @@ blogger:
beian: 蜀 ICP 备 16021362 号
# footer显示名称及tab标题: Deepzz's Blog
btitle: Deepzz's Blog
# 版权声明: 本站使用「<a href="//creativecommons.org/licenses/by/4.0/">署名 4.0 国际</a>」创作共享协议,转载请注明作者及原网址。
# 版权声明
copyright: 本站使用「<a href="//creativecommons.org/licenses/by/4.0/">署名 4.0 国际</a>」创作共享协议,转载请注明作者及原网址。

View File

@@ -45,9 +45,9 @@ server {
# 根证书 + 中间证书
# https://imququ.com/post/why-can-not-turn-on-ocsp-stapling.html
ssl_trusted_certificate /data/eiblog/conf/ssl/full_chained.pem;
ssl_trusted_certificate /data/eiblog/conf/ssl/full_chained.pem;
resolver 8.8.8.8 114.114.114.114 valid=300s;
resolver 114.114.114.114 8.8.8.8 valid=300s;
resolver_timeout 10s;
access_log /data/eiblog/logdata/nginx.log;
@@ -66,6 +66,7 @@ server {
expires 1d;
}
# imququ 的上传文件相关,未用到
location ^~ /static/uploads/ {
root /home/jerry/www/imququ.com/www;
add_header Access-Control-Allow-Origin *;
@@ -146,7 +147,7 @@ server {
}
location ^~ /.well-known/acme-challenge/ {
alias /home/jerry/www/challenges/;
alias /data/letsencrypt/challenges/;
try_files $uri =404;
}
@@ -154,28 +155,3 @@ server {
rewrite ^/(.*)$ https://deepzz.com/$1 permanent;
}
}
# 原博客,内部做的转发
server {
server_name blog.deepzz.com;
access_log /dev/null;
location / {
rewrite ^/(.*)$ https://blog.deepzz.com/$1 permanent;
}
}
server {
listen 443;
server_name blog.deepzz.com;
add_header Strict-Transport-Security "max-age=31536000";
add_header X-Frame-Options deny;
add_header X-Content-Type-Options nosniff;
add_header X-Xss-Protection "1; mode=block;";
location / {
proxy_pass https://127.0.0.1:9010;
}
}

View File

@@ -0,0 +1,24 @@
# 博主原博客
server {
server_name blog.deepzz.com;
access_log /dev/null;
location / {
rewrite ^/(.*)$ https://blog.deepzz.com/$1 permanent;
}
}
server {
listen 443;
server_name blog.deepzz.com;
add_header Strict-Transport-Security "max-age=31536000";
add_header X-Frame-Options deny;
add_header X-Content-Type-Options nosniff;
add_header X-Xss-Protection "1; mode=block;";
location / {
proxy_pass https://127.0.0.1:9010;
}
}

View File

@@ -10,19 +10,20 @@ worker_processes 1;
events {
worker_connections 1024;
worker_connections 10240;
}
http {
include mime.types;
default_type application/octet-stream;
charset UTF-8;
include mime.types;
default_type application/octet-stream;
charset UTF-8;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
access_log off;
sendfile on;
tcp_nopush on;
@@ -58,74 +59,13 @@ http {
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
include /data/eiblog/conf/nginx/domain/*.conf;
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<cross-domain-policy>
<allow-access-from domain="*.{{.Domain}}" />
</cross-domain-policy>

View File

@@ -1,21 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{.Title}}</title>
<link>https://{{.Domain}}</link>
<description>{{.SubTitle}}</description>
<atom:link href="https://{{.Domain}}/rss.html" rel="self"/>
<atom:link href="{{.FeedrURL}}" rel="hub"/>
<language>zh-CN</language>
<lastBuildDate>{{.BuildDate}}</lastBuildDate>
{{range .Artcs}}
<item>
<title>{{.Title}}</title>
<link>https://{{$.Domain}}/post/{{.Slug}}.html</link>
<comments>https://{{$.Domain}}/post/{{.Slug}}.html#comments</comments>
<guid>https://{{$.Domain}}/post/{{.Slug}}.html</guid>
<description><![CDATA[<blockquote>{{.Content}}<p>本文链接:<a href="https://{{$.Domain}}/post/{{.Slug}}.html">https://{{$.Domain}}/post/{{.Slug}}.html</a><a href="https://{{$.Domain}}/post/{{.Slug}}.html#comments">参与评论 »</a></p>]]></description>
</item>
{{end}}
</channel>
<channel>
<title>{{.Title}}</title>
<link>https://{{.Domain}}</link>
<description>{{.SubTitle}}</description>
<atom:link href="https://{{.Domain}}/rss.html" rel="self" />
<atom:link href="{{.FeedrURL}}" rel="hub" />
<language>zh-CN</language>
<lastBuildDate>{{.BuildDate}}</lastBuildDate>
{{range .Artcs}}
<item>
<title>{{.Title}}</title>
<link>https://{{$.Domain}}/post/{{.Slug}}.html</link>
<comments>https://{{$.Domain}}/post/{{.Slug}}.html#comments</comments>
<guid>https://{{$.Domain}}/post/{{.Slug}}.html</guid>
<description>
<![CDATA[<blockquote>{{.Content}}<p>本文链接:<a href="https://{{$.Domain}}/post/{{.Slug}}.html">https://{{$.Domain}}/post/{{.Slug}}.html</a><a href="https://{{$.Domain}}/post/{{.Slug}}.html#comments">参与评论 »</a></p>]]>
</description>
</item>
{{end}}
</channel>
</rss>

3
conf/tpl/robotsTpl.xml Normal file
View File

@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://{{.Domain}}/sitemap.xml

16
db.go
View File

@@ -6,6 +6,7 @@ import (
"fmt"
"regexp"
"sort"
"strings"
"sync"
"time"
@@ -17,6 +18,7 @@ import (
"gopkg.in/mgo.v2/bson"
)
// 数据库及表名
const (
DB = "eiblog"
COLLECTION_ACCOUNT = "account"
@@ -29,6 +31,7 @@ const (
DELETE = "delete"
)
// blackfriday 配置
const (
commonHtmlFlags = 0 |
blackfriday.HTML_TOC |
@@ -95,7 +98,7 @@ func init() {
ms.Close()
// 读取帐号信息
Ei = loadAccount()
// 获取文章
// 获取文章数据
Ei.Articles = loadArticles()
// 生成markdown文档
go generateMarkdown()
@@ -211,7 +214,7 @@ func generateTopic() {
Title: "关于",
Slug: "about",
CreateTime: time.Now(),
UpdateTime: time.Now(),
UpdateTime: time.Time{},
}
blogroll := &Article{
ID: db.NextVal(DB, COUNTER_ARTICLE),
@@ -219,7 +222,7 @@ func generateTopic() {
Title: "友情链接",
Slug: "blogroll",
UpdateTime: time.Now(),
CreateTime: time.Now(),
CreateTime: time.Time{},
}
err := db.Insert(DB, COLLECTION_ARTICLE, blogroll)
if err != nil {
@@ -365,6 +368,12 @@ var reg = regexp.MustCompile(setting.Conf.Identifier)
var regH = regexp.MustCompile("</nav></div>")
func GenerateExcerptAndRender(artc *Article) {
if strings.HasPrefix(artc.Content, setting.Conf.Description) {
index := strings.Index(artc.Content, "\r\n")
artc.Desc = IgnoreHtmlTag(artc.Content[len(setting.Conf.Description):index])
artc.Content = artc.Content[index:]
}
content := renderPage([]byte(artc.Content))
index := regH.FindIndex(content)
if index != nil {
@@ -569,6 +578,7 @@ func QuerySerie(id int32) *Serie {
return nil
}
// 后台分页
func PageListBack(se int, kw string, draft, del bool, p, n int) (max int, artcs []*Article) {
M := bson.M{}
if draft {

View File

@@ -1,6 +1,7 @@
package main
import (
"html/template"
"testing"
)
@@ -42,3 +43,20 @@ func TestAddSerie(t *testing.T) {
t.Error(err)
}
}
func TestRenderPage(t *testing.T) {
data := []byte(`<ul class="links ssl">
<li><a href="https://yryz.net/">一人游走</a><span class="date">「不错的小伙子」</span></li>
<li><a href="https://hsulei.com/">Leo同学</a><span class="date">「小伙子,该干活了」</span></li>
<li><a href="https://razeencheng.com/">razeen同学</a><span class="date">「Stay hungry. Stay foolish.」</span></li>
</ul>
<ul class="links">
<li><a href="http://blog.mirreal.net/">Mirreal Ellison</a><span class="date">「kissing the fire」</span></li>
</ul>`)
t.Log(IgnoreHtmlTag(string(data)))
data = renderPage(data)
t.Log(template.HTML(string(data)))
}

View File

@@ -17,6 +17,7 @@ import (
type result struct {
Code int
Response []struct {
Id string
Posts int
Identifiers []string
}
@@ -66,6 +67,7 @@ func PostsCount() {
artc := Ei.MapArticles[v.Identifiers[0][i+1:]]
if artc != nil {
artc.Count = v.Posts
artc.Thread = v.Id
}
}
}

View File

@@ -6,6 +6,8 @@ services:
volumes:
- /data/eiblog/mgodb:/data/db
restart: always
ports:
- 27017:27017
elasticsearch:
image: elasticsearch:2.4.1
container_name: eisearch

20
docs/amusing.md Normal file
View File

@@ -0,0 +1,20 @@
### Twitter Card
相信很多人不明白为什么会这样专注twitter。首先twitter是一个社交网站国际性的。其次我们可以使用它的Twitter Card功能非常的酷。
当你配置好Twitter相关的参数后`conf/app.yml`
```
# twitter地址: twitter.com/chenqijing2
twitter:
card: summary
site: chenqijing2
image: st.deepzz.com/static/img/avatar.jpg
address: twitter.com/chenqijing2
```
每当你发部一个推文,你如果带上你的网址,它会自动给你展示成卡片的形式
![twitter-card](http://7xokm2.com1.z0.glb.clouddn.com/img/twitter-pub.png)
![twitter-card2](http://7xokm2.com1.z0.glb.clouddn.com/img/twitter-pub2.png)
可以看到``之前是没有内容的,该内容是我们文章的描述。

238
docs/install.md Normal file
View File

@@ -0,0 +1,238 @@
### 安装
1、`Eiblog`提供多个平台的压缩包下载,可到[Eiblog release](https://github.com/eiblog/eiblog/releases)选择相应版本和平台下载。也可通过:
``` sh
$ curl -L https://github.com/eiblog/eiblog/releases/download/v1.0.0/eiblog-v1.0.0.`uname -s | tr '[A-Z]' '[a-z]'`-amd64.tar.gz > eiblog-v1.0.0.`uname -s | tr '[A-Z]' '[a-z]'`-amd64.tar.gz
```
2、如果有幸你也是`Gopher`,相信你会亲自动手,你可以通过:
``` sh
$ go get https://github.com/eiblog/eiblog
```
进行源码编译二进制文件运行。
3、如果你对`docker`技术也有研究的话,你也可以通过`docker`来安装:
``` sh
$ docker pull registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog:v1.2.0
```
镜像内部只提供了`eiblog`的二进制文件,因为其它内容定制化的需求过高。所以需要将`conf`、`static`、`views`目录映射出来,后面会具体说到。
### 本地测试
在我们下载好可执行程序之后,我们可以开始本地测试的工作了。
本地测试需要搭建两个服务`mongodb`和`elasticsearch2.4.1`(可选,搜索服务不可用)。
`Eiblog`默认会连接`hostname`为`mongodb`和`elasticsearch`,因此你需要将信息填入`/etc/hosts`下。假如你搭建的`mongodb`地址为`127.0.0.1:27017``elasticsearch`地址为`192.168.99.100:9200`,如:
``` sh
$ sudo vi /etc/hosts
# 在末尾加上两行
172.42.0.1 mongodb
192.168.99.100 elasticsearch
```
#### MongoDB 搭建
1、`MongoDB`搭建Mac 可通过`brew install mongo`进行安装,其它平台请查询资料。
#### Elasticsearch 搭建
2、`Elasticsearch`搭建,它的搭建要些许复杂。博主尚未接触如何直接安装,因此建议通过`docker`搭建。需要注意的是 es 自带的分析器对中文分词是不友好的,这里采用了`elasticsearch-analysis-ik`分词器。如果你想了解更多[Github](https://github.com/medcl/elasticsearch-analysis-ik)或则如何实现[博客站内搜索](https://imququ.com/post/elasticsearch.html)。
* pull 镜像`docker pull elasticsearch:2.4.1`,必需使用该版本。
* 添加环境变量`ES_JAVA_OPTS: "-Xms512m -Xmx512m"`,除非你想让你的服务器爆掉。
* 映射相关目录:
```
conf/es/config:/usr/share/elasticsearch/config
conf/es/plugins:/usr/share/elasticsearch/plugins
conf/es/data:/usr/share/elasticsearch/data
conf/es/logs:/usr/share/elasticsearch/logs
```
 请将这四个目录映射至`eiblog`下的`conf`目录。如果你想查看更多,请查看`docker-compose.yml`文件。
总结一下,`docker`运行 es 的命令为:
``` sh
$ docker run -d --name eisearch \
-p 9200:9200 \
-e ES_JAVA_OPTS: "-Xms512m -Xmx512m" \
-v conf/es/config:/usr/share/elasticsearch/config \
-v conf/es/plugins:/usr/share/elasticsearch/plugins \
-v conf/es/data:/usr/share/elasticsearch/data \
-v conf/es/logs:/usr/share/elasticsearch/logs \
elasticsearch:2.4.1
```
之后执行`./eiblog`,咱们的`eiblog`就可以运行起来了。
通过`127.0.0.1:9000`可以进入博客首页,`127.0.0.1:9000/admin/login`进入后台登陆,账号密码为`eiblog/conf/app.yml`下的`username`和`password`。也就是初始账号密码`deepz`、`deepzz`。
> `注意`,因为配置`conf/app.yml`均是博主自用配置。有些操作可能(如评论)会评论到我的博客,还请尽量避免,谢谢。
### 准备部署
如果你在感受了该博客的魅力了之后,仍然坚持想要搭建它。那么,恭喜你,获得的一款不想再更换的博客系统。下面,我们跟随步骤对部署流程进一步说明。
这里只提供`Docker`的相关部署说明。你如果需要其它方式部署,请参考该方式。
#### 前提准备
这里需要准备一些必要的东西,如果你已准备好。请跳过。
* `一台服务器`。
* `一个域名`,国内服务器需备案。
* `有效的证书`。一般使用免费的就可以。如:`Lets Encrypt`,另外`qcloud`、`七牛`也提供了免费证书的申请,均是全球可信。
* `七牛CDN`。博客只设计接入了七牛cdn相信该CDN服务商不会让你失望。
* `Disqus`。作为博客评论系统,你得有翻墙的能力注册到该账号,具体配置我想又可以写一片博客了。简单说需要`shorname`和`public key`。
* `Google Analystic`。数据统计分析工具。
* `Superfeedr`。加速 RSS 订阅。
* `Twitter`。希望你能够有一个twitter账号。
是不是这么多要求,很费解。其实当初该博客系统只是为个人而设计的,是自己心中想要的那一款。博主些这篇文章不是想要多少人来用该博客,而是希望对那些追求至极的朋友说:你需要这款博客系统。
#### 文件准备
尽管大多数文件已经准备好。但有些默认的文件需要特别指出来,需要你在 CDN 上写特殊的路径。
假如你的 CDN 域名为`st.example.com`,那么请确保你的 CDN 中已经上传以下文件:
* `favicon.ico`,默认为`st.example.com/static/img/favicon.ico`。故你在 CDN 中的文件名为`static/img/favicon.ico`,以下如是。
* `bg04.jpg`,左侧背景图片,`500*1200`左右CDN 中文件名:`static/img/bg04.jpg`。如需更改,请在`eiblog/view/st_blog.css`中替换该名称。
* `avatar.jpg`,左侧头像,`160*160~256*256`之间CDN 文件名:`static/img/avatar.jpg`。另外你需要将该图片 `Base64` 编码后替换掉`eiblog/views/st_blog.css`中合适位置的图片。
* `blank.gif`CDN 文件名:`static/img/blank.gif`。该图片请从[这里](https://st.deepzz.com/static/img/blank.gif)下载并上传至你的 CDN。
* `default_avatar.png`Disqus 默认头像CDN 文件名:`static/img/default_avatar.png`,请从[这里](https://st.deepzz.com/static/img/default_avatar.png)下载并上传至你的 CDN。
* `disqus.js`,通过 https://short_name.disqus.com/embed.js 下载,替换掉 short_name。CDN 文件名格式是:`static/js/disqus_xxx.js`。在我写这篇文章是使用的是:`static/js/disqus_a9d3fd.js`。另外修改`eiblog/views/st_blog.js`中的`disqus_a9d3fd.js`为你新文件的名称。
> 注意:每次修改 views 内的以 `st_` 开头的文件,请将 `app.yml` 中的 staticversion 提高一个版本。
#### 配置说明
走到这里,我相信只走到`60%`的路程。放弃还来得及。
这里会对`eiblog/conf`下的所有文件做说明,希望你做好准备。
```
├── app.yml # 博客配置文件
├── blackip.yml # 博客ip黑名单
├── es # elasticsearch配置
│   ├── config # 配置文件
│   │   ├── analysis # 同义词
│   │   ├── elasticsearch.yml # 具体配置
│   │   ├── logging.yml # 日志配置
│   │   └── scripts # 脚本文件夹
│   └── plugins # 插件文件夹
│   └── ik1.10.1 # ik分词器
├── nginx # nginx配置
│   ├── domain # 域名配置nginx会读区改文件夹下的.conf文件
│   │   └── deepzz.conf
│   ├── ip.blacklist # nginx ip黑名单
│   └── nginx.conf # nginx配置请替换原有配置
├── scts # ct文件
│   ├── aviator.sct
│   └── digicert.sct
├── ssl # 证书文件具体请看deepzz.conf
│   ├── dhparams.pem
│   ├── domain.key
│   ├── domain.pem
│   ├── full_chained.pem
│   └── session_ticket.key
└── tpl # 模版文件
├── crossdomainTpl.xml
├── feedTpl.xml
├── opensearchTpl.xml
├── robotsTpl.xml
└── sitemapTpl.xml
```
1、app.yml整个程序的配置文件里面已经列出了所有配置项的说明这里不再阐述。
2、blackip.yml如果没有使用`Nginx`,博客内置`ip`过滤系统。
3、`es`全名`elasticsearch`,非常强大的分布式搜索引擎,`github`用的就是它。里面的配置基本不用修改,但`es/analysis/synonym.txt`是同义词,你可以照着已有的随意增加。
```
├── es
│   ├── config
│   │   ├── analysis
│   │   │   └── synonym.txt #同义词配置
│   │   ├── elasticsearch.yml #分词器配置
│   │   ├── logging.yml #日志配置
│   │   └── scripts #脚本
│   └── plugins #中文分词插件
│   └── ik1.10.0
```
> `注意`scripts文件夹虽然是空的但必需存在不然elasticsearch报错。
4、`nginx`,系统采用`nginx`作为代理(相信博客系统也不会独占一台服务器~)。请使用`nginx.conf`替换原`nginx`的配置。博客系统的配置文件是`domain/deepzz.conf`,或则重命名(只要是满足`*.conf`)。`deepzz.conf`文件里面学问是最多的。或许你想一一弄懂,或许…。
> 注意本配置需要更新nginx到最新版openssl更新到1.0.2j,具体请到 Jerry Qu 的[本博客 Nginx 配置之完整篇](https://imququ.com/post/my-nginx-conf.html)查看,了解详情。
5、`scts`,存放 ct 文件。
6、`ssl`,这里存放了所有证书相关的内容。
```
├── dhparams.pem #参见eiblog/conf/nginx/domain/deepzz.conf
├── domain.key #证书私钥,一般颁发者处下载
├── domain.pem #证书链,一般从证书颁发者那可以直接下载到
├── full_chained.pem #参见eiblog/conf/nginx/domain/deepzz.conf
└── session_ticket.key #参见eiblog/conf/nginx/domain/deepzz.conf
```
7、`tpl`模版相关,不用修改。
### 开始部署
#### docker
请确定你已经完成了上面所说的所有步骤,在本地已经测试成功。服务器上`MognoDB`和`Elasticsearch`已经安装并已经运行成功。
首先,请将本地测试好的`conf``static``views`文件夹上传至服务器,建议存储到服务器`/data/eiblog`下。
``` sh
$ tree /data/eiblog -L 1
├── conf
├── static
├── views
```
然后,将镜像 PULL 到服务器本地。
``` sh
# PULL下Eiblog镜像
$ docker pull registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
```
最后,执行`docker run`命令,希望你能成功。
``` sh
$ docker run -d --name eiblog --restart=always \
--add-host disqus.com:23.235.33.134 \
--add-host mongodb:172.42.0.1 \
--add-host elasticsearch:192.168.99.100 \
-p 9000:9000 \
-e GODEBUG=netdns=cgo \
-v /data/eiblog/logdata:/eiblog/logdata \
-v /data/eiblog/conf:/eiblog/conf \
-v /data/eiblog/static:/eiblog/static \
-v /data/eiblog/views:/eiblog/views \
registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
```
这里默认`MongDB`和`Elasticsearch`均为`docker`部署,且名称为`eidb``eisearch`。
#### nginx + docker
通过`Nginx+docker`部署,是博主推荐的方式。这里采用`Docker Compose`管理我们整个博客系统。
请确认你已经成功安装好`Nginx`、`docker`、`docker-compose`。Nginx 请一定参照 Jerry Qu 的[Nginx 配置完整篇](https://imququ.com/post/my-nginx-conf.html)。
首先,请将本地测试好的`conf``static``views``docker-compose.yml`文件夹和文件上传至服务器。前三个文件夹建议存储到服务器`/data/eiblog`下,`docker-compose.yml`存放在你使用方便的地方。
> 注意检查`conf/es/config/scripts`空文件夹是否存在
``` sh
$ tree /data/eiblog -L 1
├── conf
├── static
├── views
$ ls ~/
docker-compose.yml
```
然后,执行:
``` sh
$ cd ~
$ docker-compose up -d
```
等待些许时间,成功运行。

74
docs/writing.md Normal file
View File

@@ -0,0 +1,74 @@
### 郑重提醒
**标题**、**slug**、**内容**。在你点击保存的时候一定确保三者不能为空,否则页面刷新内容就没了。所以,养成一个良好的写作习惯很重要。
当然,博客的自动保存功能也非常的好。在你不确定是否发布前,你可以将之保存到草稿,以便下次继续编辑。
### 文章标题
文章标题,这个可能要看个人习惯。我习惯从三级标题开始(###),依次往下四级标题,五级标题...。要注意的是一定不能跳级:
```
### 标题一
#### 标题1.1
#### 标题1.2
##### 标题1.2.1
##### 标题1.2.2
### 标题二
##### 标题2.1
##### 标题2.2
###### 标题2.2.1
###### 标题2.2.2
```
结果是:
![article-title](http://7xokm2.com1.z0.glb.clouddn.com/article-title.png)
### 文章描述
文章描述,主要是给`html->head->meta`中的 name 为 description 用的。现采用了一个临时的办法:在文章的第一行通过前缀识别(只看第一行)。
该前缀可到`conf/app.yml`设置,默认为`Desc:`,如:
![article-description](http://7xokm2.com1.z0.glb.clouddn.com/img/article-description.png)
### 图片懒加载
博客系统提供图片懒加载功能(浏览到某个位置,图片才会加载),以此来提高页面加载速度。我们可根据需要是否使用。
当然由此带来的坏处就是rss不能够正确加载图片。后续看是否解决这个问题或朋友提PR。
首先看下图片的`markdown`标准写法:
```
![alt](img_addres)
```
如:
```
![sublime-dialog](https://st.deepzz.com/blog/img/dialog-box-without-all-contols.png)
```
![sublime-dialog](https://st.deepzz.com/blog/img/dialog-box-without-all-contols.png)
懒加载,需要为该图片指定大小(长高):
```
![alt](img_addres =widthxheight)
```
x 为小写字母x,y,z中的 x。使页面未加载时也占了相应的位置大小这样设计是为了让读者在浏览页面时不会感到抖动。
如:
```
![sublime-dialog](https://st.deepzz.com/blog/img/dialog-box-without-all-contols.png =640x301)
```
### 摘要截取
摘要截取主要是提供给首页显示,如:
![home-page](http://7xokm2.com1.z0.glb.clouddn.com/img/deepzz_home_page.jpg)
红框中圈出来的就是截取出来的内容。在 `conf/app.yml` 的配置项有两个:
```
# 自动截取预览, 字符数
length: 400
# 截取预览标识
identifier: <!--more-->
```
当程序不能检查到 identifier 的标识符时,会采用长度的方式进行截取。

View File

@@ -71,6 +71,7 @@ func HandleNotFound(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
h["Title"] = "Not Found"
h["Description"] = "404 Not Found"
h["Path"] = ""
c.Status(http.StatusNotFound)
RenderHTMLFront(c, "notfound", h)
@@ -80,6 +81,7 @@ func HandleHomePage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
h["Title"] = Ei.BTitle + " | " + Ei.SubTitle
h["Description"] = "博客首页," + Ei.SubTitle
h["Path"] = c.Request.URL.Path
h["CurrentPage"] = "blog-home"
pn, err := strconv.Atoi(c.Query("pn"))
@@ -95,6 +97,7 @@ func HandleSeriesPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
h["Title"] = "专题 | " + Ei.BTitle
h["Description"] = "专题列表," + Ei.SubTitle
h["Path"] = c.Request.URL.Path
h["CurrentPage"] = "series"
h["Article"] = Ei.PageSeries
@@ -106,6 +109,7 @@ func HandleArchivesPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
h["Title"] = "归档 | " + Ei.BTitle
h["Description"] = "博客归档," + Ei.SubTitle
h["Path"] = c.Request.URL.Path
h["CurrentPage"] = "archives"
h["Article"] = Ei.PageArchives
@@ -115,11 +119,11 @@ func HandleArchivesPage(c *gin.Context) {
func HandleArticlePage(c *gin.Context) {
path := c.Param("slug")
artc := Ei.MapArticles[path[0:strings.Index(path, ".")]]
if artc == nil {
if !strings.HasSuffix(path, ".html") || Ei.MapArticles[path[:len(path)-5]] == nil {
HandleNotFound(c)
return
}
artc := Ei.MapArticles[path[:len(path)-5]]
h := GetBase()
h["Version"] = StaticVersion(c)
h["Title"] = artc.Title + " | " + Ei.BTitle
@@ -128,9 +132,12 @@ func HandleArticlePage(c *gin.Context) {
var name string
if path == "blogroll.html" {
name = "blogroll"
h["Description"] = "友情连接," + Ei.SubTitle
} else if path == "about.html" {
name = "about"
h["Description"] = "关于作者," + Ei.SubTitle
} else {
h["Description"] = artc.Desc + "" + Ei.SubTitle
name = "article"
h["Copyright"] = Ei.Copyright
if !artc.UpdateTime.IsZero() {
@@ -151,6 +158,7 @@ func HandleSearchPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
h["Title"] = "站内搜索 | " + Ei.BTitle
h["Description"] = "站内搜索," + Ei.SubTitle
h["Path"] = ""
h["CurrentPage"] = "search-post"
@@ -199,6 +207,7 @@ func HandleDisqusFrom(c *gin.Context) {
"Title": "发表评论 | " + Ei.BTitle,
"ATitle": artc.Title,
"Thread": params[1],
"Slug": artc.Slug,
}
err := Tmpl.ExecuteTemplate(c.Writer, "disqus.html", data)
if err != nil {
@@ -243,6 +252,7 @@ func HandleBeacon(c *gin.Context) {
return
}
req.Header.Set("User-Agent", ua)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
logd.Error(err)
@@ -267,7 +277,7 @@ type DisqusComments struct {
ErrMsg string `json:"errmsg"`
Data struct {
Next string `json:"next"`
Total int `json:"total,omitempty"`
Total int `json:"total"`
Comments []commentsDetail `json:"comments"`
Thread string `json:"thread"`
} `json:"data"`
@@ -287,7 +297,11 @@ type commentsDetail struct {
func HandleDisqus(c *gin.Context) {
slug := c.Param("slug")
cursor := c.Query("cursor")
dcs := DisqusComments{}
if artc := Ei.MapArticles[slug]; artc != nil {
dcs.Data.Thread = artc.Thread
}
postsList := PostsList(slug, cursor)
if postsList != nil {
dcs.ErrNo = postsList.Code

24
glide.lock generated
View File

@@ -1,8 +1,8 @@
hash: 4b70e76a2e830e97033c06d0e5a90c3199985ff5070bdf8364b1feca63d5caa5
updated: 2016-12-25T00:17:00.257473303+08:00
hash: bd360fa297ed66950543990f9433cdcdf13c29dd99d9a01b49027e236b2cb9da
updated: 2017-06-14T20:34:29.429161691+08:00
imports:
- name: github.com/boj/redistore
version: fc113767cd6b051980f260d6dbe84b2740c46ab0
version: 4562487a4bee9a7c272b72bfaeda4917d0a47ab9
- name: github.com/eiblog/blackfriday
version: c0ec111761ae784fe31cc076f2fa0e2d2216d623
- name: github.com/eiblog/utils
@@ -13,12 +13,12 @@ imports:
- tmpl
- uuid
- name: github.com/garyburd/redigo
version: f8c71fc158ba13d50a7f5d8f10ea18ec49463c73
version: 95d11dba2d44531bdb8022752b98912baafae03a
subpackages:
- internal
- redis
- name: github.com/gin-gonic/contrib
version: 7b9bbbaec78850441ed357ac702e474b26c08f9b
version: d4fc5a96cc0d29cb0e862bb1312dd6f4fedfcaee
subpackages:
- sessions
- name: github.com/gin-gonic/gin
@@ -33,21 +33,21 @@ imports:
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/securecookie
version: fa5329f913702981df43dcb2a380bac429c810b5
version: e59506cc896acb7f7bf732d4fdf5e25f7ccd8983
- name: github.com/gorilla/sessions
version: 83c8db3bdc9be789e57e3756ffbcffd2d7d40176
version: 8b6b4cd75f07f7ee036eb37b8127bd40ab1efc49
- name: github.com/manucorporat/sse
version: ee05b128a739a0fb76c7ebd3ae4810c1de808d6d
- name: github.com/mattn/go-isatty
version: 30a891c33c7cde7b02a981314b4228ec99380cca
version: fc9e8d8ef48496124e79ae0df75490096eccf6fe
- name: github.com/shurcooL/sanitized_anchor_name
version: 1dba4b3954bc059efc3991ec364f9f9a35f597d2
version: 541ff5ee47f1dddf6a5281af78307d921524bcb5
- name: golang.org/x/net
version: f315505cf3349909cdf013ea56690da34e96a451
subpackages:
- context
- name: golang.org/x/sys
version: d75a52659825e75fff6158388dddc6a5b04f9ba5
version: 0b25a408a50076fbbcae6b7ac0ea5fbb0b085e79
subpackages:
- unix
- name: gopkg.in/go-playground/validator.v8
@@ -60,9 +60,9 @@ imports:
- internal/sasl
- internal/scram
- name: gopkg.in/yaml.v2
version: a5b47d31c556af34a302ce5d659e6fea44d90de0
version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b
- name: qiniupkg.com/api.v7
version: 7cfd4b639917bf924d8c1cd17a6d61175e809066
version: 89344a711feec1d800c77a80d0865de936dc394d
subpackages:
- api
- auth/qbox

View File

@@ -6,11 +6,20 @@ import:
- logd
- mgo
- tmpl
- uuid
- package: github.com/gin-gonic/contrib
subpackages:
- sessions
- package: github.com/gin-gonic/gin
version: ~1.1.4
- package: gopkg.in/mgo.v2
subpackages:
- bson
- package: gopkg.in/yaml.v2
- package: qiniupkg.com/api.v7
subpackages:
- kodo
- kodocli
- package: qiniupkg.com/x
subpackages:
- url.v7

View File

@@ -119,6 +119,10 @@ type Article struct {
Header string `bson:"-"`
// 预览信息
Excerpt string `bson:"-"`
// 一句话描述,文章第一句
Desc string `bson:"-"`
// disqus thread
Thread string `bson:"-"`
}
type SortArticles []*Article

View File

@@ -27,6 +27,7 @@ type Config struct {
PageSize int // 后台每页文章数量
Length int // 自动截取预览长度
Identifier string // 截取标示
Description string // 文章描述前缀
Favicon string // icon地址
StartID int32 // 文章起始id
SearchURL string // elasticsearch 地址

View File

@@ -1,6 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" />
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

View File

@@ -1,6 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://deepzz.com/sitemap.xml
User-agent: MJ12bot
Disallow: /

2
vendor/github.com/boj/redistore/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
.DS_Store

19
vendor/github.com/boj/redistore/LICENSE generated vendored Normal file
View 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
View 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
``` go
// 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
View File

@@ -0,0 +1,4 @@
/*
Package redistore is a session store backend for gorilla/sessions
*/
package redistore

361
vendor/github.com/boj/redistore/redistore.go generated vendored Normal file
View File

@@ -0,0 +1,361 @@
// 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
ok bool
)
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
View 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{})
}

View 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.

View 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
View 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
View 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
View 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).

View 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)]
}

View 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")
}

View 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
View 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
View 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
View 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
View 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
View 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
}

424
vendor/github.com/garyburd/redigo/redis/pool.go generated vendored Normal file
View File

@@ -0,0 +1,424 @@
// 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 connections in the pool. The count includes idle connections and connections in use.
func (p *Pool) ActiveCount() int {
p.mu.Lock()
active := p.active
p.mu.Unlock()
return active
}
// IdleCount returns the number of idle connections in the pool.
func (p *Pool) IdleCount() int {
p.mu.Lock()
idle := p.idle.Len()
p.mu.Unlock()
return idle
}
// 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 }

688
vendor/github.com/garyburd/redigo/redis/pool_test.go generated vendored Normal file
View File

@@ -0,0 +1,688 @@
// 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, inuse 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)
}
if idle := p.IdleCount(); idle != open-inuse {
d.t.Errorf("%s: idle=%d, want %d", message, idle, open-inuse)
}
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, 0)
p.Close()
d.check("after close", p, 2, 0, 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, 0)
p.Close()
d.check("after close", p, 12, 0, 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, 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, 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, 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, 0)
now = now.Add(p.IdleTimeout)
c = p.Get()
c.Do("PING")
c.Close()
d.check("2", p, 2, 1, 0)
}
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, 0)
}
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, 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, 2)
c2.Close()
d.check("3", p, 2, 2, 1)
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, 1)
}
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, 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, 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, 0)
}
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, 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, 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, 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, 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, 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, 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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}
}

91
vendor/github.com/garyburd/redigo/redis/script.go generated vendored Normal file
View File

@@ -0,0 +1,91 @@
// 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
}
// Hash returns the script hash.
func (s *Script) Hash() string {
return s.hash
}
// 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
View 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
View 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()
}())
}

View File

@@ -0,0 +1,114 @@
// 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
View 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()
}

View 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
View 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"

2
vendor/github.com/gin-gonic/contrib/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
*/Godeps/*
!*/Godeps/Godeps.json

18
vendor/github.com/gin-gonic/contrib/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,18 @@
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- tip
services:
- memcache
- redis-server
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/acc2c57482e94b44f557
on_success: change
on_failure: always
on_start: false

36
vendor/github.com/gin-gonic/contrib/README.md generated vendored Normal file
View File

@@ -0,0 +1,36 @@
# gin-gonic/contrib
[![Build Status](https://travis-ci.org/gin-gonic/contrib.svg)](https://travis-ci.org/gin-gonic/contrib)
Here you'll find middleware ready to use with [Gin Framework](https://github.com/gin-gonic/gin). Submit your pull request, either with the package in a folder, or by adding a link to this `README.md`.
If adding a package directly, don't forget to create a `README.md` inside with author name.
If adding a link to your own repository, please follow this example:
```
+ nameOfMiddleware (https://github.com/yourusername/yourrepo)
```
Each author is responsible of maintaining his own code, although if you submit as a package, you allow the community to fix it. You can also submit a pull request to fix an existing package.
## List of external middleware
+ [RestGate](https://github.com/pjebs/restgate) - Secure authentication for REST API endpoints
+ [staticbin](https://github.com/olebedev/staticbin) - middleware/handler for serving static files from binary data
+ [gin-cors](https://github.com/gin-contrib/cors) - Official CORS gin's middleware
+ [gin-csrf](https://github.com/utrack/gin-csrf) - CSRF protection
+ [gin-health](https://github.com/utrack/gin-health) - middleware that simplifies stat reporting via [gocraft/health](https://github.com/gocraft/health)
+ [gin-merry](https://github.com/utrack/gin-merry) - middleware for pretty-printing [merry](https://github.com/ansel1/merry) errors with context
+ [gin-revision](https://github.com/appleboy/gin-revision-middleware) - Revision middleware for Gin framework
+ [gin-jwt](https://github.com/appleboy/gin-jwt) - JWT Middleware for Gin Framework
+ [gin-sessions](https://github.com/kimiazhu/ginweb-contrib/tree/master/sessions) - session middleware based on mongodb and mysql
+ [gin-location](https://github.com/drone/gin-location) - middleware for exposing the server's hostname and scheme
+ [gin-nice-recovery](https://github.com/ekyoung/gin-nice-recovery) - panic recovery middleware that let's you build a nicer user experience
+ [gin-limit](https://github.com/aviddiviner/gin-limit) - limits simultaneous requests; can help with high traffic load
+ [ez-gin-template](https://github.com/michelloworld/ez-gin-template) - easy template wrap for gin
+ [gin-hydra](https://github.com/janekolszak/gin-hydra) - [Hydra](https://github.com/ory-am/hydra) middleware for Gin
+ [gin-glog](https://github.com/zalando/gin-glog) - meant as drop-in replacement for Gin's default logger
+ [gin-gomonitor](https://github.com/zalando/gin-gomonitor) - for exposing metrics with Go-Monitor
+ [gin-oauth2](https://github.com/zalando/gin-oauth2) - for working with OAuth2
+ [static](https://github.com/hyperboloide/static) An alternative static assets handler for the gin framework.
+ [xss-mw](https://github.com/dvwright/xss-mw) - XssMw is a middleware designed to "auto remove XSS" from user submitted input

View File

@@ -0,0 +1,22 @@
{
"ImportPath": "github.com/gin-gonic/contrib/cache",
"GoVersion": "go1.3",
"Deps": [
{
"ImportPath": "github.com/bradfitz/gomemcache/memcache",
"Rev": "72a68649ba712ee7c4b5b4a943a626bcd7d90eb8"
},
{
"ImportPath": "github.com/garyburd/redigo/redis",
"Rev": "535138d7bcd717d6531c701ef5933d98b1866257"
},
{
"ImportPath": "github.com/gin-gonic/gin",
"Rev": "ac0ad2fed865d40a0adc1ac3ccaadc3acff5db4b"
},
{
"ImportPath": "github.com/robfig/go-cache",
"Rev": "9fc39e0dbf62c034ec4e45e6120fc69433a3ec51"
}
]
}

5
vendor/github.com/gin-gonic/contrib/cache/README.md generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# cache
## EOL-warning
**This package has been abandoned on 2016-12-07. Please use [gin-contrib/cache](https://github.com/gin-contrib/cache) instead.**

153
vendor/github.com/gin-gonic/contrib/cache/cache.go generated vendored Normal file
View File

@@ -0,0 +1,153 @@
package cache
import (
"bytes"
"crypto/sha1"
"errors"
"github.com/gin-gonic/gin"
"io"
"net/http"
"net/url"
"time"
)
const (
DEFAULT = time.Duration(0)
FOREVER = time.Duration(-1)
CACHE_MIDDLEWARE_KEY = "gincontrib.cache"
)
var (
PageCachePrefix = "gincontrib.page.cache"
ErrCacheMiss = errors.New("cache: key not found.")
ErrNotStored = errors.New("cache: not stored.")
ErrNotSupport = errors.New("cache: not support.")
)
type CacheStore interface {
Get(key string, value interface{}) error
Set(key string, value interface{}, expire time.Duration) error
Add(key string, value interface{}, expire time.Duration) error
Replace(key string, data interface{}, expire time.Duration) error
Delete(key string) error
Increment(key string, data uint64) (uint64, error)
Decrement(key string, data uint64) (uint64, error)
Flush() error
}
type responseCache struct {
status int
header http.Header
data []byte
}
type cachedWriter struct {
gin.ResponseWriter
status int
written bool
store CacheStore
expire time.Duration
key string
}
func urlEscape(prefix string, u string) string {
key := url.QueryEscape(u)
if len(key) > 200 {
h := sha1.New()
io.WriteString(h, u)
key = string(h.Sum(nil))
}
var buffer bytes.Buffer
buffer.WriteString(prefix)
buffer.WriteString(":")
buffer.WriteString(key)
return buffer.String()
}
func newCachedWriter(store CacheStore, expire time.Duration, writer gin.ResponseWriter, key string) *cachedWriter {
return &cachedWriter{writer, 0, false, store, expire, key}
}
func (w *cachedWriter) WriteHeader(code int) {
w.status = code
w.written = true
w.ResponseWriter.WriteHeader(code)
}
func (w *cachedWriter) Status() int {
return w.status
}
func (w *cachedWriter) Written() bool {
return w.written
}
func (w *cachedWriter) Write(data []byte) (int, error) {
ret, err := w.ResponseWriter.Write(data)
if err == nil {
//cache response
store := w.store
val := responseCache{
w.status,
w.Header(),
data,
}
err = store.Set(w.key, val, w.expire)
if err != nil {
// need logger
}
}
return ret, err
}
// Cache Middleware
func Cache(store *CacheStore) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set(CACHE_MIDDLEWARE_KEY, store)
c.Next()
}
}
func SiteCache(store CacheStore, expire time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
var cache responseCache
url := c.Request.URL
key := urlEscape(PageCachePrefix, url.RequestURI())
if err := store.Get(key, &cache); err != nil {
c.Next()
} else {
c.Writer.WriteHeader(cache.status)
for k, vals := range cache.header {
for _, v := range vals {
c.Writer.Header().Add(k, v)
}
}
c.Writer.Write(cache.data)
}
}
}
// Cache Decorator
func CachePage(store CacheStore, expire time.Duration, handle gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
var cache responseCache
url := c.Request.URL
key := urlEscape(PageCachePrefix, url.RequestURI())
if err := store.Get(key, &cache); err != nil {
// replace writer
writer := newCachedWriter(store, expire, c.Writer, key)
c.Writer = writer
handle(c)
} else {
c.Writer.WriteHeader(cache.status)
for k, vals := range cache.header {
for _, v := range vals {
c.Writer.Header().Add(k, v)
}
}
c.Writer.Write(cache.data)
}
}
}

202
vendor/github.com/gin-gonic/contrib/cache/cache_test.go generated vendored Normal file
View File

@@ -0,0 +1,202 @@
package cache
import (
"math"
"testing"
"time"
)
type cacheFactory func(*testing.T, time.Duration) CacheStore
// Test typical cache interactions
func typicalGetSet(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)
value := "foo"
if err = cache.Set("value", value, DEFAULT); err != nil {
t.Errorf("Error setting a value: %s", err)
}
value = ""
err = cache.Get("value", &value)
if err != nil {
t.Errorf("Error getting a value: %s", err)
}
if value != "foo" {
t.Errorf("Expected to get foo back, got %s", value)
}
}
// Test the increment-decrement cases
func incrDecr(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)
// Normal increment / decrement operation.
if err = cache.Set("int", 10, DEFAULT); err != nil {
t.Errorf("Error setting int: %s", err)
}
newValue, err := cache.Increment("int", 50)
if err != nil {
t.Errorf("Error incrementing int: %s", err)
}
if newValue != 60 {
t.Errorf("Expected 60, was %d", newValue)
}
if newValue, err = cache.Decrement("int", 50); err != nil {
t.Errorf("Error decrementing: %s", err)
}
if newValue != 10 {
t.Errorf("Expected 10, was %d", newValue)
}
// Increment wraparound
newValue, err = cache.Increment("int", math.MaxUint64-5)
if err != nil {
t.Errorf("Error wrapping around: %s", err)
}
if newValue != 4 {
t.Errorf("Expected wraparound 4, got %d", newValue)
}
// Decrement capped at 0
newValue, err = cache.Decrement("int", 25)
if err != nil {
t.Errorf("Error decrementing below 0: %s", err)
}
if newValue != 0 {
t.Errorf("Expected capped at 0, got %d", newValue)
}
}
func expiration(t *testing.T, newCache cacheFactory) {
// memcached does not support expiration times less than 1 second.
var err error
cache := newCache(t, time.Second)
// Test Set w/ DEFAULT
value := 10
cache.Set("int", value, DEFAULT)
time.Sleep(2 * time.Second)
err = cache.Get("int", &value)
if err != ErrCacheMiss {
t.Errorf("Expected CacheMiss, but got: %s", err)
}
// Test Set w/ short time
cache.Set("int", value, time.Second)
time.Sleep(2 * time.Second)
err = cache.Get("int", &value)
if err != ErrCacheMiss {
t.Errorf("Expected CacheMiss, but got: %s", err)
}
// Test Set w/ longer time.
cache.Set("int", value, time.Hour)
time.Sleep(2 * time.Second)
err = cache.Get("int", &value)
if err != nil {
t.Errorf("Expected to get the value, but got: %s", err)
}
// Test Set w/ forever.
cache.Set("int", value, FOREVER)
time.Sleep(2 * time.Second)
err = cache.Get("int", &value)
if err != nil {
t.Errorf("Expected to get the value, but got: %s", err)
}
}
func emptyCache(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)
err = cache.Get("notexist", 0)
if err == nil {
t.Errorf("Error expected for non-existent key")
}
if err != ErrCacheMiss {
t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err)
}
err = cache.Delete("notexist")
if err != ErrCacheMiss {
t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err)
}
_, err = cache.Increment("notexist", 1)
if err != ErrCacheMiss {
t.Errorf("Expected cache miss incrementing non-existent key: %s", err)
}
_, err = cache.Decrement("notexist", 1)
if err != ErrCacheMiss {
t.Errorf("Expected cache miss decrementing non-existent key: %s", err)
}
}
func testReplace(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)
// Replace in an empty cache.
if err = cache.Replace("notexist", 1, FOREVER); err != ErrNotStored {
t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err)
}
// Set a value of 1, and replace it with 2
if err = cache.Set("int", 1, time.Second); err != nil {
t.Errorf("Unexpected error: %s", err)
}
if err = cache.Replace("int", 2, time.Second); err != nil {
t.Errorf("Unexpected error: %s", err)
}
var i int
if err = cache.Get("int", &i); err != nil {
t.Errorf("Unexpected error getting a replaced item: %s", err)
}
if i != 2 {
t.Errorf("Expected 2, got %d", i)
}
// Wait for it to expire and replace with 3 (unsuccessfully).
time.Sleep(2 * time.Second)
if err = cache.Replace("int", 3, time.Second); err != ErrNotStored {
t.Errorf("Expected ErrNotStored, got: %s", err)
}
if err = cache.Get("int", &i); err != ErrCacheMiss {
t.Errorf("Expected cache miss, got: %s", err)
}
}
func testAdd(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)
// Add to an empty cache.
if err = cache.Add("int", 1, time.Second); err != nil {
t.Errorf("Unexpected error adding to empty cache: %s", err)
}
// Try to add again. (fail)
if err = cache.Add("int", 2, time.Second); err != ErrNotStored {
t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err)
}
// Wait for it to expire, and add again.
time.Sleep(2 * time.Second)
if err = cache.Add("int", 3, time.Second); err != nil {
t.Errorf("Unexpected error adding to cache: %s", err)
}
// Get and verify the value.
var i int
if err = cache.Get("int", &i); err != nil {
t.Errorf("Unexpected error: %s", err)
}
if i != 3 {
t.Errorf("Expected 3, got: %d", i)
}
}

View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"github.com/gin-gonic/contrib/cache"
"github.com/gin-gonic/gin"
"time"
)
func main() {
r := gin.Default()
store := cache.NewInMemoryStore(time.Second)
// Cached Page
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong "+fmt.Sprint(time.Now().Unix()))
})
r.GET("/cache_ping", cache.CachePage(store, time.Minute, func(c *gin.Context) {
c.String(200, "pong "+fmt.Sprint(time.Now().Unix()))
}))
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}

78
vendor/github.com/gin-gonic/contrib/cache/inmemory.go generated vendored Normal file
View File

@@ -0,0 +1,78 @@
package cache
import (
"github.com/robfig/go-cache"
"reflect"
"time"
)
type InMemoryStore struct {
cache.Cache
}
func NewInMemoryStore(defaultExpiration time.Duration) *InMemoryStore {
return &InMemoryStore{*cache.New(defaultExpiration, time.Minute)}
}
func (c *InMemoryStore) Get(key string, value interface{}) error {
val, found := c.Cache.Get(key)
if !found {
return ErrCacheMiss
}
v := reflect.ValueOf(value)
if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() {
v.Elem().Set(reflect.ValueOf(val))
return nil
}
return ErrNotStored
}
func (c *InMemoryStore) Set(key string, value interface{}, expires time.Duration) error {
// NOTE: go-cache understands the values of DEFAULT and FOREVER
c.Cache.Set(key, value, expires)
return nil
}
func (c *InMemoryStore) Add(key string, value interface{}, expires time.Duration) error {
err := c.Cache.Add(key, value, expires)
if err == cache.ErrKeyExists {
return ErrNotStored
}
return err
}
func (c *InMemoryStore) Replace(key string, value interface{}, expires time.Duration) error {
if err := c.Cache.Replace(key, value, expires); err != nil {
return ErrNotStored
}
return nil
}
func (c *InMemoryStore) Delete(key string) error {
if found := c.Cache.Delete(key); !found {
return ErrCacheMiss
}
return nil
}
func (c *InMemoryStore) Increment(key string, n uint64) (uint64, error) {
newValue, err := c.Cache.Increment(key, n)
if err == cache.ErrCacheMiss {
return 0, ErrCacheMiss
}
return newValue, err
}
func (c *InMemoryStore) Decrement(key string, n uint64) (uint64, error) {
newValue, err := c.Cache.Decrement(key, n)
if err == cache.ErrCacheMiss {
return 0, ErrCacheMiss
}
return newValue, err
}
func (c *InMemoryStore) Flush() error {
c.Cache.Flush()
return nil
}

View File

@@ -0,0 +1,35 @@
package cache
import (
"testing"
"time"
)
var newInMemoryStore = func(_ *testing.T, defaultExpiration time.Duration) CacheStore {
return NewInMemoryStore(defaultExpiration)
}
// Test typical cache interactions
func TestInMemoryCache_TypicalGetSet(t *testing.T) {
typicalGetSet(t, newInMemoryStore)
}
func TestInMemoryCache_IncrDecr(t *testing.T) {
incrDecr(t, newInMemoryStore)
}
func TestInMemoryCache_Expiration(t *testing.T) {
expiration(t, newInMemoryStore)
}
func TestInMemoryCache_EmptyCache(t *testing.T) {
emptyCache(t, newInMemoryStore)
}
func TestInMemoryCache_Replace(t *testing.T) {
testReplace(t, newInMemoryStore)
}
func TestInMemoryCache_Add(t *testing.T) {
testAdd(t, newInMemoryStore)
}

87
vendor/github.com/gin-gonic/contrib/cache/memcached.go generated vendored Normal file
View File

@@ -0,0 +1,87 @@
package cache
import (
"github.com/bradfitz/gomemcache/memcache"
"time"
)
type MemcachedStore struct {
*memcache.Client
defaultExpiration time.Duration
}
func NewMemcachedStore(hostList []string, defaultExpiration time.Duration) *MemcachedStore {
return &MemcachedStore{memcache.New(hostList...), defaultExpiration}
}
func (c *MemcachedStore) Set(key string, value interface{}, expires time.Duration) error {
return c.invoke((*memcache.Client).Set, key, value, expires)
}
func (c *MemcachedStore) Add(key string, value interface{}, expires time.Duration) error {
return c.invoke((*memcache.Client).Add, key, value, expires)
}
func (c *MemcachedStore) Replace(key string, value interface{}, expires time.Duration) error {
return c.invoke((*memcache.Client).Replace, key, value, expires)
}
func (c *MemcachedStore) Get(key string, value interface{}) error {
item, err := c.Client.Get(key)
if err != nil {
return convertMemcacheError(err)
}
return deserialize(item.Value, value)
}
func (c *MemcachedStore) Delete(key string) error {
return convertMemcacheError(c.Client.Delete(key))
}
func (c *MemcachedStore) Increment(key string, delta uint64) (uint64, error) {
newValue, err := c.Client.Increment(key, delta)
return newValue, convertMemcacheError(err)
}
func (c *MemcachedStore) Decrement(key string, delta uint64) (uint64, error) {
newValue, err := c.Client.Decrement(key, delta)
return newValue, convertMemcacheError(err)
}
func (c *MemcachedStore) Flush() error {
return ErrNotSupport
}
func (c *MemcachedStore) invoke(storeFn func(*memcache.Client, *memcache.Item) error,
key string, value interface{}, expire time.Duration) error {
switch expire {
case DEFAULT:
expire = c.defaultExpiration
case FOREVER:
expire = time.Duration(0)
}
b, err := serialize(value)
if err != nil {
return err
}
return convertMemcacheError(storeFn(c.Client, &memcache.Item{
Key: key,
Value: b,
Expiration: int32(expire / time.Second),
}))
}
func convertMemcacheError(err error) error {
switch err {
case nil:
return nil
case memcache.ErrCacheMiss:
return ErrCacheMiss
case memcache.ErrNotStored:
return ErrNotStored
}
return err
}

View File

@@ -0,0 +1,46 @@
package cache
import (
"net"
"testing"
"time"
)
// These tests require memcached running on localhost:11211 (the default)
const testServer = "localhost:11211"
var newMemcachedStore = func(t *testing.T, defaultExpiration time.Duration) CacheStore {
c, err := net.Dial("tcp", testServer)
if err == nil {
c.Write([]byte("flush_all\r\n"))
c.Close()
return NewMemcachedStore([]string{testServer}, defaultExpiration)
}
t.Errorf("couldn't connect to memcached on %s", testServer)
t.FailNow()
panic("")
}
func TestMemcachedCache_TypicalGetSet(t *testing.T) {
typicalGetSet(t, newMemcachedStore)
}
func TestMemcachedCache_IncrDecr(t *testing.T) {
incrDecr(t, newMemcachedStore)
}
func TestMemcachedCache_Expiration(t *testing.T) {
expiration(t, newMemcachedStore)
}
func TestMemcachedCache_EmptyCache(t *testing.T) {
emptyCache(t, newMemcachedStore)
}
func TestMemcachedCache_Replace(t *testing.T) {
testReplace(t, newMemcachedStore)
}
func TestMemcachedCache_Add(t *testing.T) {
testAdd(t, newMemcachedStore)
}

181
vendor/github.com/gin-gonic/contrib/cache/redis.go generated vendored Normal file
View File

@@ -0,0 +1,181 @@
package cache
import (
"github.com/garyburd/redigo/redis"
"time"
)
// Wraps the Redis client to meet the Cache interface.
type RedisStore struct {
pool *redis.Pool
defaultExpiration time.Duration
}
// until redigo supports sharding/clustering, only one host will be in hostList
func NewRedisCache(host string, password string, defaultExpiration time.Duration) *RedisStore {
var pool = &redis.Pool{
MaxIdle: 5,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
// the redis protocol should probably be made sett-able
c, err := redis.Dial("tcp", host)
if err != nil {
return nil, err
}
if len(password) > 0 {
if _, err := c.Do("AUTH", password); err != nil {
c.Close()
return nil, err
}
} else {
// check with PING
if _, err := c.Do("PING"); err != nil {
c.Close()
return nil, err
}
}
return c, err
},
// custom connection test method
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if _, err := c.Do("PING"); err != nil {
return err
}
return nil
},
}
return &RedisStore{pool, defaultExpiration}
}
func (c *RedisStore) Set(key string, value interface{}, expires time.Duration) error {
return c.invoke(c.pool.Get().Do, key, value, expires)
}
func (c *RedisStore) Add(key string, value interface{}, expires time.Duration) error {
conn := c.pool.Get()
if exists(conn, key) {
return ErrNotStored
}
return c.invoke(conn.Do, key, value, expires)
}
func (c *RedisStore) Replace(key string, value interface{}, expires time.Duration) error {
conn := c.pool.Get()
if !exists(conn, key) {
return ErrNotStored
}
err := c.invoke(conn.Do, key, value, expires)
if value == nil {
return ErrNotStored
} else {
return err
}
}
func (c *RedisStore) Get(key string, ptrValue interface{}) error {
conn := c.pool.Get()
defer conn.Close()
raw, err := conn.Do("GET", key)
if raw == nil {
return ErrCacheMiss
}
item, err := redis.Bytes(raw, err)
if err != nil {
return err
}
return deserialize(item, ptrValue)
}
func exists(conn redis.Conn, key string) bool {
retval, _ := redis.Bool(conn.Do("EXISTS", key))
return retval
}
func (c *RedisStore) Delete(key string) error {
conn := c.pool.Get()
defer conn.Close()
if !exists(conn, key) {
return ErrCacheMiss
}
_, err := conn.Do("DEL", key)
return err
}
func (c *RedisStore) Increment(key string, delta uint64) (uint64, error) {
conn := c.pool.Get()
defer conn.Close()
// Check for existance *before* increment as per the cache contract.
// redis will auto create the key, and we don't want that. Since we need to do increment
// ourselves instead of natively via INCRBY (redis doesn't support wrapping), we get the value
// and do the exists check this way to minimize calls to Redis
val, err := conn.Do("GET", key)
if val == nil {
return 0, ErrCacheMiss
}
if err == nil {
currentVal, err := redis.Int64(val, nil)
if err != nil {
return 0, err
}
var sum int64 = currentVal + int64(delta)
_, err = conn.Do("SET", key, sum)
if err != nil {
return 0, err
}
return uint64(sum), nil
} else {
return 0, err
}
}
func (c *RedisStore) Decrement(key string, delta uint64) (newValue uint64, err error) {
conn := c.pool.Get()
defer conn.Close()
// Check for existance *before* increment as per the cache contract.
// redis will auto create the key, and we don't want that, hence the exists call
if !exists(conn, key) {
return 0, ErrCacheMiss
}
// Decrement contract says you can only go to 0
// so we go fetch the value and if the delta is greater than the amount,
// 0 out the value
currentVal, err := redis.Int64(conn.Do("GET", key))
if err == nil && delta > uint64(currentVal) {
tempint, err := redis.Int64(conn.Do("DECRBY", key, currentVal))
return uint64(tempint), err
}
tempint, err := redis.Int64(conn.Do("DECRBY", key, delta))
return uint64(tempint), err
}
func (c *RedisStore) Flush() error {
conn := c.pool.Get()
defer conn.Close()
_, err := conn.Do("FLUSHALL")
return err
}
func (c *RedisStore) invoke(f func(string, ...interface{}) (interface{}, error),
key string, value interface{}, expires time.Duration) error {
switch expires {
case DEFAULT:
expires = c.defaultExpiration
case FOREVER:
expires = time.Duration(0)
}
b, err := serialize(value)
if err != nil {
return err
}
conn := c.pool.Get()
defer conn.Close()
if expires > 0 {
_, err := f("SETEX", key, int32(expires/time.Second), b)
return err
} else {
_, err := f("SET", key, b)
return err
}
}

View File

@@ -0,0 +1,48 @@
package cache
import (
"net"
"testing"
"time"
)
// These tests require redis server running on localhost:6379 (the default)
const redisTestServer = "localhost:6379"
var newRedisStore = func(t *testing.T, defaultExpiration time.Duration) CacheStore {
c, err := net.Dial("tcp", redisTestServer)
if err == nil {
c.Write([]byte("flush_all\r\n"))
c.Close()
redisCache := NewRedisCache(redisTestServer, "", defaultExpiration)
redisCache.Flush()
return redisCache
}
t.Errorf("couldn't connect to redis on %s", redisTestServer)
t.FailNow()
panic("")
}
func TestRedisCache_TypicalGetSet(t *testing.T) {
typicalGetSet(t, newRedisStore)
}
func TestRedisCache_IncrDecr(t *testing.T) {
incrDecr(t, newRedisStore)
}
func TestRedisCache_Expiration(t *testing.T) {
expiration(t, newRedisStore)
}
func TestRedisCache_EmptyCache(t *testing.T) {
emptyCache(t, newRedisStore)
}
func TestRedisCache_Replace(t *testing.T) {
testReplace(t, newRedisStore)
}
func TestRedisCache_Add(t *testing.T) {
testAdd(t, newRedisStore)
}

View File

@@ -0,0 +1,66 @@
package cache
import (
"bytes"
"encoding/gob"
"reflect"
"strconv"
)
func serialize(value interface{}) ([]byte, error) {
if bytes, ok := value.([]byte); ok {
return bytes, nil
}
switch v := reflect.ValueOf(value); v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return []byte(strconv.FormatInt(v.Int(), 10)), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return []byte(strconv.FormatUint(v.Uint(), 10)), nil
}
var b bytes.Buffer
encoder := gob.NewEncoder(&b)
if err := encoder.Encode(value); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func deserialize(byt []byte, ptr interface{}) (err error) {
if bytes, ok := ptr.(*[]byte); ok {
*bytes = byt
return nil
}
if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr {
switch p := v.Elem(); p.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var i int64
i, err = strconv.ParseInt(string(byt), 10, 64)
if err != nil {
return err
} else {
p.SetInt(i)
}
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
var i uint64
i, err = strconv.ParseUint(string(byt), 10, 64)
if err != nil {
return err
} else {
p.SetUint(i)
}
return nil
}
}
b := bytes.NewBuffer(byt)
decoder := gob.NewDecoder(b)
if err = decoder.Decode(ptr); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,53 @@
package commonlog
import (
"bytes"
"io"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// Instances a Logger middleware that will write the logs to gin.DefaultWriter
// By default gin.DefaultWriter = os.Stdout
func New() gin.HandlerFunc {
return NewWithWriter(gin.DefaultWriter)
}
// Instance a Logger middleware with the specified writter buffer.
// Example: os.Stdout, a file opened in write mode, a socket...
func NewWithWriter(out io.Writer) gin.HandlerFunc {
pool := &sync.Pool{
New: func() interface{} {
buf := new(bytes.Buffer)
return buf
},
}
return func(c *gin.Context) {
// Process request
c.Next()
//127.0.0.1 user-identifier frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
w := pool.Get().(*bytes.Buffer)
w.Reset()
w.WriteString(c.ClientIP())
w.WriteString(" - - ")
w.WriteString(time.Now().Format("[02/Jan/2006:15:04:05 -0700] "))
w.WriteString("\"")
w.WriteString(c.Request.Method)
w.WriteString(" ")
w.WriteString(c.Request.URL.Path)
w.WriteString(" ")
w.WriteString(c.Request.Proto)
w.WriteString("\" ")
w.WriteString(strconv.Itoa(c.Writer.Status()))
w.WriteString(" ")
w.WriteString(strconv.Itoa(c.Writer.Size()))
w.WriteString("\n")
w.WriteTo(out)
pool.Put(w)
}
}

5
vendor/github.com/gin-gonic/contrib/cors/README.md generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# cors
## EOL-warning
**This package has been abandoned on 2016-12-07. Please use [gin-contrib/cors](https://github.com/gin-contrib/cors) instead.**

109
vendor/github.com/gin-gonic/contrib/cors/config.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
package cors
import (
"net/http"
"strconv"
"strings"
"time"
)
type settings struct {
allowAllOrigins bool
allowedOriginFunc func(string) bool
allowedOrigins []string
allowedMethods []string
allowedHeaders []string
exposedHeaders []string
normalHeaders http.Header
preflightHeaders http.Header
}
func newSettings(c Config) *settings {
if err := c.Validate(); err != nil {
panic(err.Error())
}
return &settings{
allowedOriginFunc: c.AllowOriginFunc,
allowAllOrigins: c.AllowAllOrigins,
allowedOrigins: c.AllowedOrigins,
allowedMethods: distinct(c.AllowedMethods),
allowedHeaders: distinct(c.AllowedHeaders),
normalHeaders: generateNormalHeaders(c),
preflightHeaders: generatePreflightHeaders(c),
}
}
func (c *settings) validateOrigin(origin string) (string, bool) {
if c.allowAllOrigins {
return "*", true
}
if c.allowedOriginFunc != nil {
return origin, c.allowedOriginFunc(origin)
}
for _, value := range c.allowedOrigins {
if value == origin {
return origin, true
}
}
return "", false
}
func (c *settings) validateMethod(method string) bool {
// TODO!!!
return true
}
func (c *settings) validateHeader(header string) bool {
// TODO!!!
return true
}
func generateNormalHeaders(c Config) http.Header {
headers := make(http.Header)
if c.AllowCredentials {
headers.Set("Access-Control-Allow-Credentials", "true")
}
if len(c.ExposedHeaders) > 0 {
headers.Set("Access-Control-Expose-Headers", strings.Join(c.ExposedHeaders, ", "))
}
return headers
}
func generatePreflightHeaders(c Config) http.Header {
headers := make(http.Header)
if c.AllowCredentials {
headers.Set("Access-Control-Allow-Credentials", "true")
}
if len(c.AllowedMethods) > 0 {
headers.Set("Access-Control-Allow-Methods", strings.Join(c.AllowedMethods, ", "))
}
if len(c.AllowedHeaders) > 0 {
headers.Set("Access-Control-Allow-Headers", strings.Join(c.AllowedHeaders, ", "))
}
if c.MaxAge > time.Duration(0) {
headers.Set("Access-Control-Max-Age", strconv.FormatInt(int64(c.MaxAge/time.Second), 10))
}
return headers
}
func distinct(s []string) []string {
m := map[string]bool{}
for _, v := range s {
if _, seen := m[v]; !seen {
s[len(m)] = v
m[v] = true
}
}
return s[:len(m)]
}
func parse(content string) []string {
if len(content) == 0 {
return nil
}
parts := strings.Split(content, ",")
for i := 0; i < len(parts); i++ {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
}

145
vendor/github.com/gin-gonic/contrib/cors/cors.go generated vendored Normal file
View File

@@ -0,0 +1,145 @@
package cors
import (
"errors"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type Config struct {
AbortOnError bool
AllowAllOrigins bool
// AllowedOrigins is a list of origins a cross-domain request can be executed from.
// If the special "*" value is present in the list, all origins will be allowed.
// Default value is ["*"]
AllowedOrigins []string
// AllowOriginFunc is a custom function to validate the origin. It take the origin
// as argument and returns true if allowed or false otherwise. If this option is
// set, the content of AllowedOrigins is ignored.
AllowOriginFunc func(origin string) bool
// AllowedMethods is a list of methods the client is allowed to use with
// cross-domain requests. Default value is simple methods (GET and POST)
AllowedMethods []string
// AllowedHeaders is list of non simple headers the client is allowed to use with
// cross-domain requests.
// If the special "*" value is present in the list, all headers will be allowed.
// Default value is [] but "Origin" is always appended to the list.
AllowedHeaders []string
// ExposedHeaders indicates which headers are safe to expose to the API of a CORS
// API specification
ExposedHeaders []string
// AllowCredentials indicates whether the request can include user credentials like
// cookies, HTTP authentication or client side SSL certificates.
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached
MaxAge time.Duration
}
func (c *Config) AddAllowedMethods(methods ...string) {
c.AllowedMethods = append(c.AllowedMethods, methods...)
}
func (c *Config) AddAllowedHeaders(headers ...string) {
c.AllowedHeaders = append(c.AllowedHeaders, headers...)
}
func (c *Config) AddExposedHeaders(headers ...string) {
c.ExposedHeaders = append(c.ExposedHeaders, headers...)
}
func (c Config) Validate() error {
if c.AllowAllOrigins && (c.AllowOriginFunc != nil || len(c.AllowedOrigins) > 0) {
return errors.New("conflict settings: all origins are allowed. AllowOriginFunc or AllowedOrigins is not needed")
}
if !c.AllowAllOrigins && c.AllowOriginFunc == nil && len(c.AllowedOrigins) == 0 {
return errors.New("conflict settings: all origins disabled")
}
if c.AllowOriginFunc != nil && len(c.AllowedOrigins) > 0 {
return errors.New("conflict settings: if a allow origin func is provided, AllowedOrigins is not needed")
}
for _, origin := range c.AllowedOrigins {
if !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://") {
return errors.New("bad origin: origins must include http:// or https://")
}
}
return nil
}
var defaultConfig = Config{
AbortOnError: false,
AllowAllOrigins: true,
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "HEAD"},
AllowedHeaders: []string{"Content-Type"},
//ExposedHeaders: "",
AllowCredentials: false,
MaxAge: 12 * time.Hour,
}
func DefaultConfig() Config {
cp := defaultConfig
return cp
}
func Default() gin.HandlerFunc {
return New(defaultConfig)
}
func New(config Config) gin.HandlerFunc {
s := newSettings(config)
// Algorithm based in http://www.html5rocks.com/static/images/cors_server_flowchart.png
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if len(origin) == 0 {
return
}
origin, valid := s.validateOrigin(origin)
if valid {
if c.Request.Method == "OPTIONS" {
valid = handlePreflight(c, s)
} else {
valid = handleNormal(c, s)
}
}
if !valid {
if config.AbortOnError {
c.AbortWithStatus(http.StatusForbidden)
}
return
}
c.Header("Access-Control-Allow-Origin", origin)
}
}
func handlePreflight(c *gin.Context, s *settings) bool {
c.AbortWithStatus(200)
if !s.validateMethod(c.Request.Header.Get("Access-Control-Request-Method")) {
return false
}
if !s.validateHeader(c.Request.Header.Get("Access-Control-Request-Header")) {
return false
}
for key, value := range s.preflightHeaders {
c.Writer.Header()[key] = value
}
return true
}
func handleNormal(c *gin.Context, s *settings) bool {
for key, value := range s.normalHeaders {
c.Writer.Header()[key] = value
}
return true
}

104
vendor/github.com/gin-gonic/contrib/cors/cors_test.go generated vendored Normal file
View File

@@ -0,0 +1,104 @@
package cors
import (
"net/http"
"net/http/httptest"
"testing"
"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 TestBadConfig(t *testing.T) {
assert.Panics(t, func() { New(Config{}) })
assert.Panics(t, func() {
New(Config{
AllowAllOrigins: true,
AllowedOrigins: []string{"http://google.com"},
})
})
assert.Panics(t, func() {
New(Config{
AllowAllOrigins: true,
AllowOriginFunc: func(origin string) bool { return false },
})
})
assert.Panics(t, func() {
New(Config{
AllowedOrigins: []string{"http://google.com"},
AllowOriginFunc: func(origin string) bool { return false },
})
})
assert.Panics(t, func() {
New(Config{
AllowedOrigins: []string{"google.com"},
})
})
}
func TestDeny0(t *testing.T) {
called := false
router := gin.Default()
router.Use(New(Config{
AllowedOrigins: []string{"http://example.com"},
}))
router.GET("/", func(c *gin.Context) {
called = true
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("Origin", "https://example.com")
router.ServeHTTP(w, req)
assert.True(t, called)
assert.NotContains(t, w.Header(), "Access-Control")
}
func TestDenyAbortOnError(t *testing.T) {
called := false
router := gin.Default()
router.Use(New(Config{
AbortOnError: true,
AllowedOrigins: []string{"http://example.com"},
}))
router.GET("/", func(c *gin.Context) {
called = true
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("Origin", "https://example.com")
router.ServeHTTP(w, req)
assert.False(t, called)
assert.NotContains(t, w.Header(), "Access-Control")
}
func TestDeny2(t *testing.T) {
}
func TestDeny3(t *testing.T) {
}
func TestPasses0(t *testing.T) {
}
func TestPasses1(t *testing.T) {
}
func TestPasses2(t *testing.T) {
}

27
vendor/github.com/gin-gonic/contrib/expvar/README.md generated vendored Normal file
View File

@@ -0,0 +1,27 @@
# expvar for gin
## EOL-warning
**This package has been abandoned on 2016-12-15. Please use [gin-contrib/expvar](https://github.com/gin-contrib/expvar) instead.**
```go
package main
import "github.com/gin-gonic/gin"
import "github.com/gin-gonic/contrib/expvar"
func main() {
router := gin.Default()
router.GET("/debug/vars", expvar.Handler())
router.Run(":8080")
}
```
Request: `http://localhost:8080/debug/vars`
```json
{
"cmdline": ["/var/folders/zg/q__7tncn7kxc34pc3j0_v7rc0000gn/T/go-build115008999/command-line-arguments/_obj/exe/main"],
"memstats": {"Alloc":406968,"TotalAlloc":3025088,"Sys":3999992,"Lookups":15,"Mallocs":10405,"Frees":9228,"HeapAlloc":406968,"HeapSys":1916928,"HeapIdle":1097728,"HeapInuse":819200,"HeapReleased":0,"HeapObjects":1177,"StackInuse":180224,"StackSys":180224,"MSpanInuse":7904,"MSpanSys":16384,"MCacheInuse":1200,"MCacheSys":16384,"BuckHashSys":1441424,"GCSys":137622,"OtherSys":291026,"NextGC":578160,"LastGC":1432428478423798618,"PauseTotalNs":4326675,"PauseNs":[105780,76617,91326,115727,195752,249831,554025,485129,344607,416552,400537,424639,463089,403064,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"PauseEnd":[1432428359211210066,1432428359211415512,1432428359211821428,1432428359212452447,1432428359212906768,1432428359214282772,1432428381213398837,1432428383977712300,1432428452748952359,1432428470574839824,1432428472452814302,1432428474379491025,1432428476329036668,1432428478423798444,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"NumGC":14,"EnableGC":true,"DebugGC":false,"BySize":[{"Size":0,"Mallocs":0,"Frees":0},{"Size":8,"Mallocs":258,"Frees":237},{"Size":16,"Mallocs":4125,"Frees":3670},{"Size":32,"Mallocs":765,"Frees":652},{"Size":48,"Mallocs":495,"Frees":367},{"Size":64,"Mallocs":305,"Frees":274},{"Size":80,"Mallocs":37,"Frees":24},{"Size":96,"Mallocs":32,"Frees":25},{"Size":112,"Mallocs":713,"Frees":569},{"Size":128,"Mallocs":283,"Frees":259},{"Size":144,"Mallocs":91,"Frees":85},{"Size":160,"Mallocs":139,"Frees":112},{"Size":176,"Mallocs":349,"Frees":315},{"Size":192,"Mallocs":0,"Frees":0},{"Size":208,"Mallocs":223,"Frees":196},{"Size":224,"Mallocs":2,"Frees":2},{"Size":240,"Mallocs":86,"Frees":80},{"Size":256,"Mallocs":21,"Frees":18},{"Size":288,"Mallocs":130,"Frees":104},{"Size":320,"Mallocs":19,"Frees":15},{"Size":352,"Mallocs":278,"Frees":259},{"Size":384,"Mallocs":5,"Frees":5},{"Size":416,"Mallocs":17,"Frees":10},{"Size":448,"Mallocs":0,"Frees":0},{"Size":480,"Mallocs":2,"Frees":1},{"Size":512,"Mallocs":16,"Frees":12},{"Size":576,"Mallocs":92,"Frees":83},{"Size":640,"Mallocs":12,"Frees":7},{"Size":704,"Mallocs":2,"Frees":2},{"Size":768,"Mallocs":0,"Frees":0},{"Size":896,"Mallocs":15,"Frees":12},{"Size":1024,"Mallocs":10,"Frees":8},{"Size":1152,"Mallocs":88,"Frees":82},{"Size":1280,"Mallocs":7,"Frees":6},{"Size":1408,"Mallocs":2,"Frees":1},{"Size":1536,"Mallocs":0,"Frees":0},{"Size":1664,"Mallocs":9,"Frees":4},{"Size":2048,"Mallocs":10,"Frees":9},{"Size":2304,"Mallocs":88,"Frees":82},{"Size":2560,"Mallocs":6,"Frees":5},{"Size":2816,"Mallocs":2,"Frees":1},{"Size":3072,"Mallocs":1,"Frees":1},{"Size":3328,"Mallocs":4,"Frees":1},{"Size":4096,"Mallocs":172,"Frees":167},{"Size":4608,"Mallocs":93,"Frees":81},{"Size":5376,"Mallocs":7,"Frees":0},{"Size":6144,"Mallocs":87,"Frees":81},{"Size":6400,"Mallocs":0,"Frees":0},{"Size":6656,"Mallocs":1,"Frees":0},{"Size":6912,"Mallocs":0,"Frees":0},{"Size":8192,"Mallocs":6,"Frees":6},{"Size":8448,"Mallocs":0,"Frees":0},{"Size":8704,"Mallocs":1,"Frees":1},{"Size":9472,"Mallocs":0,"Frees":0},{"Size":10496,"Mallocs":0,"Frees":0},{"Size":12288,"Mallocs":1,"Frees":1},{"Size":13568,"Mallocs":0,"Frees":0},{"Size":14080,"Mallocs":0,"Frees":0},{"Size":16384,"Mallocs":0,"Frees":0},{"Size":16640,"Mallocs":0,"Frees":0},{"Size":17664,"Mallocs":1,"Frees":0}]}
}
```

26
vendor/github.com/gin-gonic/contrib/expvar/expvar.go generated vendored Normal file
View File

@@ -0,0 +1,26 @@
package expvar
import (
"expvar"
"fmt"
"github.com/gin-gonic/gin"
)
func Handler() gin.HandlerFunc {
return func(c *gin.Context) {
w := c.Writer
c.Header("Content-Type", "application/json; charset=utf-8")
w.Write([]byte("{\n"))
first := true
expvar.Do(func(kv expvar.KeyValue) {
if !first {
w.Write([]byte(",\n"))
}
first = false
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
})
w.Write([]byte("\n}\n"))
c.AbortWithStatus(200)
}
}

View File

@@ -0,0 +1,10 @@
{
"ImportPath": "github.com/gin-gonic/contrib/ginrus",
"GoVersion": "go1.3",
"Deps": [
{
"ImportPath": "github.com/gin-gonic/gin",
"Rev": "ac0ad2fed865d40a0adc1ac3ccaadc3acff5db4b"
}
]
}

View File

@@ -0,0 +1,38 @@
package main
import (
"fmt"
"os"
"time"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/contrib/ginrus"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
// Add a ginrus middleware, which:
// - Logs all requests, like a combined access and error log.
// - Logs to stdout.
// - RFC3339 with UTC time format.
r.Use(ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true))
// Add similar middleware, but:
// - Only logs requests with errors, like an error log.
// - Logs to stderr instead of stdout.
// - Local time zone instead of UTC.
logger := logrus.New()
logger.Level = logrus.ErrorLevel
logger.Out = os.Stderr
r.Use(ginrus.Ginrus(logger, time.RFC3339, false))
// Example ping request.
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong "+fmt.Sprint(time.Now().Unix()))
})
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}

51
vendor/github.com/gin-gonic/contrib/ginrus/ginrus.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
// Package ginrus provides log handling using logrus package.
//
// Based on github.com/stephenmuss/ginerus but adds more options.
package ginrus
import (
"time"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
)
// Ginrus returns a gin.HandlerFunc (middleware) that logs requests using logrus.
//
// Requests with errors are logged using logrus.Error().
// Requests without errors are logged using logrus.Info().
//
// It receives:
// 1. A time package format string (e.g. time.RFC3339).
// 2. A boolean stating whether to use UTC time zone or local.
func Ginrus(logger *logrus.Logger, timeFormat string, utc bool) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// some evil middlewares modify this values
path := c.Request.URL.Path
c.Next()
end := time.Now()
latency := end.Sub(start)
if utc {
end = end.UTC()
}
entry := logger.WithFields(logrus.Fields{
"status": c.Writer.Status(),
"method": c.Request.Method,
"path": path,
"ip": c.ClientIP(),
"latency": latency,
"user-agent": c.Request.UserAgent(),
"time": end.Format(timeFormat),
})
if len(c.Errors) > 0 {
// Append error field if this is an erroneous request.
entry.Error(c.Errors.String())
} else {
entry.Info()
}
}
}

View File

@@ -0,0 +1,10 @@
{
"ImportPath": "github.com/gin-gonic/contrib/gzip",
"GoVersion": "go1.3",
"Deps": [
{
"ImportPath": "github.com/gin-gonic/gin",
"Rev": "ac0ad2fed865d40a0adc1ac3ccaadc3acff5db4b"
}
]
}

5
vendor/github.com/gin-gonic/contrib/gzip/README.md generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# gzip
## EOL-warning
**This package has been abandoned on 2016-12-07. Please use [gin-contrib/gzip](https://github.com/gin-contrib/gzip) instead.**

View File

@@ -0,0 +1,19 @@
package main
import (
"fmt"
"github.com/gin-gonic/contrib/gzip"
"github.com/gin-gonic/gin"
"time"
)
func main() {
r := gin.Default()
r.Use(gzip.Gzip(gzip.DefaultCompression))
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong "+fmt.Sprint(time.Now().Unix()))
})
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}

68
vendor/github.com/gin-gonic/contrib/gzip/gzip.go generated vendored Normal file
View File

@@ -0,0 +1,68 @@
package gzip
import (
"compress/gzip"
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
const (
BestCompression = gzip.BestCompression
BestSpeed = gzip.BestSpeed
DefaultCompression = gzip.DefaultCompression
NoCompression = gzip.NoCompression
)
func Gzip(level int) gin.HandlerFunc {
return func(c *gin.Context) {
if !shouldCompress(c.Request) {
return
}
gz, err := gzip.NewWriterLevel(c.Writer, level)
if err != nil {
return
}
c.Header("Content-Encoding", "gzip")
c.Header("Vary", "Accept-Encoding")
c.Writer = &gzipWriter{c.Writer, gz}
defer func() {
c.Header("Content-Length", "0")
gz.Close()
}()
c.Next()
}
}
type gzipWriter struct {
gin.ResponseWriter
writer *gzip.Writer
}
func (g *gzipWriter) WriteString(s string) (int, error) {
return g.writer.Write([]byte(s))
}
func (g *gzipWriter) Write(data []byte) (int, error) {
return g.writer.Write(data)
}
func shouldCompress(req *http.Request) bool {
if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
return false
}
extension := filepath.Ext(req.URL.Path)
if len(extension) < 4 { // fast path
return true
}
switch extension {
case ".png", ".gif", ".jpeg", ".jpg":
return false
default:
return true
}
}

81
vendor/github.com/gin-gonic/contrib/gzip/gzip_test.go generated vendored Normal file
View File

@@ -0,0 +1,81 @@
package gzip
import (
"compress/gzip"
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
const (
testResponse = "Gzip Test Response "
)
func newServer() *gin.Engine {
router := gin.Default()
router.Use(Gzip(DefaultCompression))
router.GET("/", func(c *gin.Context) {
c.Header("Content-Length", strconv.Itoa(len(testResponse)))
c.String(200, testResponse)
})
return router
}
func TestGzip(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Add("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
r := newServer()
r.ServeHTTP(w, req)
assert.Equal(t, w.Code, 200)
assert.Equal(t, w.Header().Get("Content-Encoding"), "gzip")
assert.Equal(t, w.Header().Get("Vary"), "Accept-Encoding")
assert.Equal(t, w.Header().Get("Content-Length"), "0")
assert.NotEqual(t, w.Body.Len(), 19)
gr, err := gzip.NewReader(w.Body)
assert.NoError(t, err)
defer gr.Close()
body, _ := ioutil.ReadAll(gr)
assert.Equal(t, string(body), testResponse)
}
func TestGzipPNG(t *testing.T) {
req, _ := http.NewRequest("GET", "/image.png", nil)
req.Header.Add("Accept-Encoding", "gzip")
router := gin.New()
router.Use(Gzip(DefaultCompression))
router.GET("/image.png", func(c *gin.Context) {
c.String(200, "this is a PNG!")
})
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, w.Code, 200)
assert.Equal(t, w.Header().Get("Content-Encoding"), "")
assert.Equal(t, w.Header().Get("Vary"), "")
assert.Equal(t, w.Body.String(), "this is a PNG!")
}
func TestNoGzip(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
r := newServer()
r.ServeHTTP(w, req)
assert.Equal(t, w.Code, 200)
assert.Equal(t, w.Header().Get("Content-Encoding"), "")
assert.Equal(t, w.Header().Get("Content-Length"), "19")
assert.Equal(t, w.Body.String(), testResponse)
}

View File

@@ -0,0 +1,22 @@
{
"ImportPath": "github.com/gin-gonic/contrib/jwt",
"GoVersion": "go1.3",
"Deps": [
{
"ImportPath": "github.com/dgrijalva/jwt-go",
"Rev": "5ca80149b9d3f8b863af0e2bb6742e608603bd99"
},
{
"ImportPath": "github.com/gin-gonic/gin",
"Rev": "ac0ad2fed865d40a0adc1ac3ccaadc3acff5db4b"
},
{
"ImportPath": "github.com/jacobsa/oglematchers",
"Rev": "3ecefc49db07722beca986d9bb71ddd026b133f0"
},
{
"ImportPath": "github.com/smartystreets/goconvey/convey",
"Rev": "958443eebb772fc6c1dcc2a44cdc6a59e1b3ff0d"
}
]
}

83
vendor/github.com/gin-gonic/contrib/jwt/README.md generated vendored Normal file
View File

@@ -0,0 +1,83 @@
JWT middleware for go gonic.
JSON Web Token (JWT) more information: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
EDIT: Below is the test for [christopherL91/Go-API](https://github.com/christopherL91/Go-API)
```go
package jwt_test
import (
"encoding/json"
. "github.com/smartystreets/goconvey/convey"
"io/ioutil"
"net/http"
"strings"
"testing"
)
type User struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Response struct {
Token string `json:"token"`
}
func createNewsUser(username, password string) *User {
return &User{username, password}
}
func TestLogin(t *testing.T) {
Convey("Should be able to login", t, func() {
user := createNewsUser("jonas", "1234")
jsondata, _ := json.Marshal(user)
post_data := strings.NewReader(string(jsondata))
req, _ := http.NewRequest("POST", "http://localhost:3000/api/login", post_data)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, _ := client.Do(req)
So(res.StatusCode, ShouldEqual, 200)
Convey("Should be able to parse body", func() {
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
So(err, ShouldBeNil)
Convey("Should be able to get json back", func() {
responseData := new(Response)
err := json.Unmarshal(body, responseData)
So(err, ShouldBeNil)
Convey("Should be able to be authorized", func() {
token := responseData.Token
req, _ := http.NewRequest("GET", "http://localhost:3000/api/auth/testAuth", nil)
req.Header.Set("Authorization", "Bearer "+token)
client = &http.Client{}
res, _ := client.Do(req)
So(res.StatusCode, ShouldEqual, 200)
})
})
})
})
Convey("Should not be able to login with false credentials", t, func() {
user := createNewsUser("jnwfkjnkfneknvjwenv", "wenknfkwnfknfknkfjnwkfenw")
jsondata, _ := json.Marshal(user)
post_data := strings.NewReader(string(jsondata))
req, _ := http.NewRequest("POST", "http://localhost:3000/api/login", post_data)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, _ := client.Do(req)
So(res.StatusCode, ShouldEqual, 401)
})
Convey("Should not be able to authorize with false credentials", t, func() {
token := ""
req, _ := http.NewRequest("GET", "http://localhost:3000/api/auth/testAuth", nil)
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
res, _ := client.Do(req)
So(res.StatusCode, ShouldEqual, 401)
})
}
```

View File

@@ -0,0 +1,49 @@
package main
import (
"time"
jwt_lib "github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/contrib/jwt"
"github.com/gin-gonic/gin"
)
var (
mysupersecretpassword = "unicornsAreAwesome"
)
func main() {
r := gin.Default()
public := r.Group("/api")
public.GET("/", func(c *gin.Context) {
// Create the token
token := jwt_lib.New(jwt_lib.GetSigningMethod("HS256"))
// Set some claims
token.Claims = jwt_lib.MapClaims{
"Id": "Christopher",
"exp": time.Now().Add(time.Hour * 1).Unix(),
}
// Sign and get the complete encoded token as a string
tokenString, err := token.SignedString([]byte(mysupersecretpassword))
if err != nil {
c.JSON(500, gin.H{"message": "Could not generate token"})
}
c.JSON(200, gin.H{"token": tokenString})
})
private := r.Group("/api/private")
private.Use(jwt.Auth(mysupersecretpassword))
/*
Set this header in your request to get here.
Authorization: Bearer `token`
*/
private.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello from private"})
})
r.Run("localhost:8080")
}

20
vendor/github.com/gin-gonic/contrib/jwt/jwt.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
package jwt
import (
jwt_lib "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/gin-gonic/gin"
)
func Auth(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
_, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt_lib.Token) (interface{}, error) {
b := ([]byte(secret))
return b, nil
})
if err != nil {
c.AbortWithError(401, err)
}
}
}

View File

@@ -0,0 +1,29 @@
package newrelic
import (
"time"
"github.com/gin-gonic/gin"
metrics "github.com/yvasiyarov/go-metrics"
"github.com/yvasiyarov/gorelic"
)
var agent *gorelic.Agent
func NewRelic(license string, appname string, verbose bool) gin.HandlerFunc {
agent = gorelic.NewAgent()
agent.NewrelicLicense = license
agent.HTTPTimer = metrics.NewTimer()
agent.CollectHTTPStat = true
agent.Verbose = verbose
agent.NewrelicName = appname
agent.Run()
return func(c *gin.Context) {
startTime := time.Now()
c.Next()
agent.HTTPTimer.UpdateSince(startTime)
}
}

View File

@@ -0,0 +1,86 @@
## EOL-warning
**This package has been abandoned on 2016-12-13. Please use [gin-contrib/multitemplate](https://github.com/gin-contrib/multitemplate) instead.**
This is a custom HTML render to support multi templates, ie. more than one `*template.Template`.
#Simple example
```go
package main
import (
"html/template"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/contrib/renders/multitemplate"
)
func main() {
router := gin.Default()
router.HTMLRender = createMyRender()
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index", data)
})
router.Run(":8080")
}
func createMyRender() multitemplate.Render {
r := multitemplate.New()
r.AddFromFiles("index", "base.html", "base.html")
r.AddFromFiles("article", "base.html", "article.html")
r.AddFromFiles("login", "base.html", "login.html")
r.AddFromFiles("dashboard", "base.html", "dashboard.html")
return r
}
```
##Advanced example
[https://elithrar.github.io/article/approximating-html-template-inheritance/](https://elithrar.github.io/article/approximating-html-template-inheritance/)
```go
package main
import (
"html/template"
"path/filepath"
"github.com/gin-gonic/contrib/renders/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(includes, layout)
r.Add(filepath.Base(layout), template.Must(template.ParseFiles(files...)))
}
return r
}
```

View File

@@ -0,0 +1,50 @@
package multitemplate
import (
"html/template"
"github.com/gin-gonic/gin/render"
)
type Render map[string]*template.Template
var _ render.HTMLRender = Render{}
func New() Render {
return make(Render)
}
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
}
func (r Render) AddFromFiles(name string, files ...string) *template.Template {
tmpl := template.Must(template.ParseFiles(files...))
r.Add(name, tmpl)
return tmpl
}
func (r Render) AddFromGlob(name, glob string) *template.Template {
tmpl := template.Must(template.ParseGlob(glob))
r.Add(name, tmpl)
return tmpl
}
func (r *Render) AddFromString(name, templateString string) *template.Template {
tmpl := template.Must(template.New("").Parse(templateString))
r.Add(name, tmpl)
return tmpl
}
func (r Render) Instance(name string, data interface{}) render.Render {
return render.HTML{
Template: r[name],
Data: data,
}
}

46
vendor/github.com/gin-gonic/contrib/rest/rest.go generated vendored Normal file
View File

@@ -0,0 +1,46 @@
package rest
import (
"github.com/gin-gonic/gin"
)
// All of the methods are the same type as HandlerFunc
// if you don't want to support any methods of CRUD, then don't implement it
type CreateSupported interface {
CreateHandler(*gin.Context)
}
type ListSupported interface {
ListHandler(*gin.Context)
}
type TakeSupported interface {
TakeHandler(*gin.Context)
}
type UpdateSupported interface {
UpdateHandler(*gin.Context)
}
type DeleteSupported interface {
DeleteHandler(*gin.Context)
}
// It defines
// POST: /path
// GET: /path
// PUT: /path/:id
// POST: /path/:id
func CRUD(group *gin.RouterGroup, path string, resource interface{}) {
if resource, ok := resource.(CreateSupported); ok {
group.POST(path, resource.CreateHandler)
}
if resource, ok := resource.(ListSupported); ok {
group.GET(path, resource.ListHandler)
}
if resource, ok := resource.(TakeSupported); ok {
group.GET(path+"/:id", resource.TakeHandler)
}
if resource, ok := resource.(UpdateSupported); ok {
group.PUT(path+"/:id", resource.UpdateHandler)
}
if resource, ok := resource.(DeleteSupported); ok {
group.DELETE(path+"/:id", resource.DeleteHandler)
}
}

Some files were not shown because too many files have changed in this diff Show More