Compare commits

...

120 Commits

Author SHA1 Message Date
chenqijing
bd69c62254 chore: replace beian domain: https://beian.miit.gov.cn 2020-12-29 11:50:36 +08:00
chenqijing
970c6068d5 chore: update go.mod 2020-12-22 11:19:12 +08:00
chenqijing
27a1f600de chore: github actions 2020-12-22 11:09:55 +08:00
chenqijing
edf0fbac51 chore: rm .travis.yml 2020-12-17 21:02:48 +08:00
Deepzz
98ca570a36 Merge pull request #24 from eiblog/deepzz0-patch-1
Create release.yml
2020-12-17 20:58:58 +08:00
Deepzz
9a526f97f8 Create release.yml 2020-12-17 20:58:27 +08:00
chenqijing
33a29f5e57 chore: update twitter & print disqus email 2020-12-17 19:15:16 +08:00
henry.chen
30ebf76eda chore(release): 1.4.9 2019-12-18 19:33:08 +08:00
Deepzz
f5c0bcdb99 Merge pull request #23 from eiblog/fix_disqus
Fix disqus
2019-12-18 19:22:11 +08:00
henry.chen
4b53da3801 chore(disqus): rm app.yml/disqus/embed config 2019-12-18 19:17:23 +08:00
henry.chen
1bdfb6abea fix(disqus): connect reset by peer 2019-12-04 13:24:48 +08:00
henry.chen
33f47d8f3a Merge branch 'master' of github.com:eiblog/eiblog 2019-12-04 10:30:26 +08:00
deepzz0
cbd0cfaaf5 fix(blogroll): blogroll.html ul style 2019-11-09 23:45:28 +08:00
henry.chen
080c992a92 chore: update vendor 2019-09-30 15:30:10 +08:00
deepzz0
371b2326ea fix(qiniu): empty file name 2019-03-05 00:41:43 +08:00
deepzz0
2720d11b23 Merge branch 'master' of github.com:eiblog/eiblog 2019-03-03 15:06:33 +08:00
deepzz0
b7751d7b9e fix: qiniu upload file 2019-03-03 15:06:16 +08:00
henry.chen
24d81db8be add vendor & update README.md 2018-11-19 10:24:54 +08:00
deepzz0
010137ebf5 change admin avatar.jpg->avatar.png & fix duplicate id in html 2018-09-18 21:20:13 +08:00
deepzz0
c6a2439c54 use go1.11 with go mod 2018-08-25 18:29:00 +08:00
deepzz0
1d54ff3ac5 fix modify blogroll.html and about.html archived in archive.html 2018-07-17 22:11:11 +08:00
henry.chen
63a4d69209 nginx config: use Expect-CT repleace HPKP 2018-07-17 10:32:54 +08:00
Deepzz
b35d7de58a Merge pull request #14 from vyloy/master
fix doSitemap bug
2018-07-06 22:05:12 +08:00
Michael(Zhiyi Weng)
77ea01b7c1 fix doSitemap bug 2018-07-06 10:02:38 +08:00
Razeen
5f608b638d Update README.md 2018-05-17 18:42:00 +08:00
deepzz0
52da8abceb update 2018-05-07 20:51:30 +08:00
deepzz0
f016b28cb6 fix comments duration 2018-05-07 20:45:37 +08:00
henry.chen
01b7643ca5 Merge branch 'master' of github.com:eiblog/eiblog 2018-05-07 16:52:56 +08:00
henry.chen
375d43761b let's encrypt v2 embedded ct,rm about cert's ct 2018-05-07 16:51:54 +08:00
Deepzz
f3e9727947 Set theme jekyll-theme-cayman 2018-05-02 09:45:41 +08:00
Deepzz
911aa963c7 Update eiblog.conf
X-Real_IP -> X-Real-IP
2018-03-27 10:48:16 +08:00
henry.chen
fb66b6871e release v1.4.3 2018-02-09 16:15:34 +08:00
henry.chen
5ae76f243e fixed #6,发布文章异步提交,随机 session key等 2018-02-09 13:50:34 +08:00
deepzz0
051b034e51 1. 修复编辑专题:按钮显示"新增专题"错误 2. 编辑专题链接移动到专题名称 2018-02-04 12:39:35 +08:00
Deepzz
27439ecc71 Update install.md 2018-02-01 21:24:00 +08:00
henry.chen
d02c838447 fix archive page bug 2018-01-25 23:09:59 +08:00
Deepzz
d17acf5325 Update amusing.md 2018-01-17 19:16:08 +08:00
deepzz0
b278ca377f update changelog.md 2018-01-14 13:53:32 +08:00
deepzz0
93131441e4 update 2018-01-14 13:38:26 +08:00
deepzz0
ddcc6c2d2e auto archiving by year when the month great than 12 2018-01-14 13:12:59 +08:00
henry.chen
ef63ae9598 fix page archive unable auto update 2018-01-14 02:40:11 +08:00
henry.chen
2ed9db5c7b code logical adjust 2018-01-14 02:02:12 +08:00
deepzz0
06a12bc6f9 update vendor 2018-01-13 18:23:03 +08:00
deepzz0
6524b45751 adjust the code 2018-01-13 18:19:54 +08:00
henry.chen
ceb9e2690b 添加 disqus thread 创建接口 2018-01-13 02:56:35 +08:00
deepzz0
405fbaf24f fix can delete blogroll and about page & fix delete and readd article bug 2018-01-07 20:30:14 +08:00
deepzz0
3245c0e0d3 update vendor & fix upload file url & fix judge file type 2018-01-06 23:24:27 +08:00
Deepzz
badc62e3f0 Update README.md 2018-01-06 11:47:20 +08:00
deepzz0
a5561f257b comment docker-compose.yml backup 2018-01-02 20:21:45 +08:00
deepzz0
eb37b83ebd update README.md 2018-01-01 19:03:16 +08:00
deepzz0
b2fab703fc Merge branch 'master' of github.com:eiblog/eiblog 2018-01-01 18:59:30 +08:00
deepzz0
37deb390d9 docker-compose.yml 添加数据库备份镜像 2018-01-01 18:59:10 +08:00
Deepzz
6fa5088352 更新 ct 服务器地址 2017-12-30 13:50:19 +08:00
Deepzz
e023a33786 Update app.yml
移除 disqus 评论及 Google 分析私人信息配置
2017-12-08 12:19:01 +08:00
henry.chen
6f818c4b5d fix search.html <no value> 2017-12-05 15:08:32 +08:00
henry.chen
9ad22fb2d9 don't use dynamic link: CGO_ENABLED=0 2017-11-30 10:04:54 +08:00
henry.chen
fc37d5e093 fix page:admin/write-post autocomplete tag 2017-11-29 16:17:58 +08:00
henry.chen
61024bfebd update 2017-11-27 18:34:03 +08:00
henry.chen
f20c4a6063 fix docker image: exec user process caused "no such file or directory" 2017-11-27 18:17:41 +08:00
henry.chen
c24e6bf7bd update .travis.yml 2017-11-27 16:43:30 +08:00
henry.chen
ade94168d3 update .travis.yml 2017-11-27 16:32:39 +08:00
henry.chen
552d010650 fix background turn page 2017-11-27 15:21:28 +08:00
deepzz0
1c3106cbb0 update vendor 2017-11-24 22:58:59 +08:00
henry.chen
168937f1b2 fix gopkg.in/mgo import conflict 2017-11-23 13:57:20 +08:00
henry.chen
730cffcb5b 修复文章自动保存bug+发布文章不成功bug 2017-11-17 13:30:47 +08:00
deepzz0
8c3f1c2aba update travis.yml 2017-11-05 13:03:52 +08:00
deepzz0
ea375ea76c update travis.yml 2017-11-05 12:56:46 +08:00
deepzz0
275a6c0c31 update travis.yml 2017-11-05 12:46:01 +08:00
deepzz0
360204995d 使用github的七牛SDK,配置名称Kodo->Qiniu 2017-11-05 12:27:22 +08:00
deepzz0
c9fc0cc75a Merge branch 'master' of github.com:eiblog/eiblog 2017-10-19 20:23:45 +08:00
deepzz0
41daaa322e fix mod date panic 2017-10-19 20:23:36 +08:00
Deepzz
894535fbe5 Update README.md 2017-10-10 20:16:01 -05:00
Deepzz
6fc5af1b0f Update eiblog.conf 2017-09-26 22:42:49 -05:00
henry.chen
5ce806a7d7 挑战 acme.sh 文件验证路径 2017-08-25 18:01:37 +08:00
Deepzz
25cb23fdb3 Update README.md 2017-08-20 17:48:44 +08:00
deepzz0
a89a1a2bc9 update 2017-08-19 14:26:19 +08:00
deepzz0
93e170f9ac fix es/config/scripts 2017-08-19 14:25:28 +08:00
Deepzz
59d9a616aa Update README.md 2017-08-17 17:00:52 +08:00
Deepzz
2ff0934206 Update install.md 2017-08-15 21:31:34 +08:00
Deepzz
cde7cba2f0 Update README.md 2017-08-15 21:23:12 +08:00
Deepzz
2be7501afe Update README.md 2017-08-15 21:12:03 +08:00
deepzz0
487d35dae2 add comments 2017-08-08 20:59:45 +08:00
henry.chen
19af9376cb add comments 2017-08-08 12:45:58 +08:00
deepzz0
3ddd2a0b33 fix disqus 基础评论bug 2017-08-08 01:03:10 +08:00
Deepzz
ee7523b124 Update helper.go 2017-08-07 18:07:58 +08:00
Deepzz
cc1dbac1f0 clean eiblog.conf 2017-07-27 22:03:43 +08:00
deepzz0
04532ba8a6 fix conflict 2017-07-26 22:48:16 +08:00
deepzz0
0a2a132b11 rm some cod in domain.cnf 2017-07-26 22:45:54 +08:00
Deepzz
3ff712d407 Update eiblog.conf 2017-07-25 09:22:08 +08:00
deepzz0
27162d2205 fix unuse tag <!--more-->
intercept errors
2017-07-15 13:46:29 +08:00
deepzz0
f150974566 rm .travis.yml about glide 2017-07-13 21:29:23 +08:00
deepzz0
b94fc825b3 updage docs 2017-07-13 21:23:28 +08:00
deepzz0
d8f0e30285 update 2017-07-13 20:31:00 +08:00
deepzz0
e0a5f0ebca avatar img use cache 2017-07-13 01:37:35 +08:00
deepzz0
c18d9c0bef update vendor 2017-07-11 23:50:01 +08:00
deepzz0
e1ec5cd08a update vendor 2017-07-09 03:33:28 +08:00
deepzz0
5efdd72e58 update docs 2017-07-09 03:24:46 +08:00
deepzz0
b9470fa14c fix and update config 2017-07-09 01:18:07 +08:00
deepzz0
a932d2906d update .dockerignore 2017-07-09 00:27:46 +08:00
deepzz0
3ff5977941 update vendor and use single static 2017-07-08 21:54:39 +08:00
deepzz0
da7b726e8d rename mode.domain-> mode.domains 2017-07-08 16:29:47 +08:00
deepzz0
3923bc70f1 update vendor 2017-07-08 12:16:15 +08:00
deepzz0
8dc73fd67c replace cert pin-sha256: trustasai G6 -> trustasia G5 2017-07-06 23:28:04 +08:00
deepzz0
2825bbfeae 完善 disqus 评论 2017-06-26 23:51:13 +08:00
deepzz0
66811830b0 update Makefile 2017-06-25 15:22:33 +08:00
deepzz0
c4bf59ce5d update 2017-06-25 06:51:44 +08:00
deepzz0
6cea283f86 添加 Makefile,使用 acme.sh 自动更新证书 2017-06-25 06:48:19 +08:00
deepzz0
3992db49ba update 2017-06-24 20:58:21 +08:00
deepzz0
055c2307cb update config 2017-06-24 19:21:53 +08:00
Deepzz
11b0f486cd Delete goblog.conf 2017-06-24 16:56:42 +08:00
Deepzz
ed0a50b626 Update eiblog.conf 2017-06-24 16:34:22 +08:00
deepzz0
cf2b2d6d34 rename 2017-06-24 16:12:00 +08:00
deepzz0
00456806bb 添加双证书配置 2017-06-24 16:11:28 +08:00
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
1586 changed files with 165922 additions and 236323 deletions

View File

@@ -1,18 +1,16 @@
.git
conf
vendor
setting
conf
static
views
docs
!static/tzdata
static/*.*
!static/favicon.ico
**/.DS_Store
Dockerfile
glide.yaml
glide.lock
*.yml
*.md
*.go
*.sh
*.DS_Store
.gitignore
.dockerignore

86
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: release image & asset
on:
push:
tags:
- "v*"
jobs:
package:
runs-on: ubuntu-16.04
steps:
- name: Golang env
uses: actions/setup-go@v2
with:
go-version: ^1.15
- name: Checkout
uses: actions/checkout@v2
- name: Cache mod
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Docker tag
id: vars
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:10})
- name: Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker login
uses: docker/login-action@v1
with:
registry: registry.cn-hangzhou.aliyuncs.com
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKER_USERNAME }}
- name: Build binary
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: |
registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog:latest
registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog:${{ steps.vars.outputs.tag }}
- name: Package tar
run: ./dist.sh
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.vars.outputs.tag }}
release_name: Release ${{ steps.vars.outputs.tag }}
draft: false
prerelease: false
- name: Upload asset darwin
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: eiblog-${{ steps.vars.outputs.tag }}.darwin-amd64.tar.gz
asset_name: eiblog-${{ steps.vars.outputs.tag }}.darwin-amd64.tar.gz
asset_content_type: application/gzip
- name: Upload asset linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: eiblog-${{ steps.vars.outputs.tag }}.linux-amd64.tar.gz
asset_name: eiblog-${{ steps.vars.outputs.tag }}.linux-amd64.tar.gz
asset_content_type: application/gzip
- name: Upload asset windows
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: eiblog-${{ steps.vars.outputs.tag }}.windows-amd64.tar.gz
asset_name: eiblog-${{ steps.vars.outputs.tag }}.windows-amd64.tar.gz
asset_content_type: application/gzip

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
*.DS_Store
**/.DS_Store
*.exe
conf/ssl/domain.*
eiblog

View File

@@ -1,45 +0,0 @@
sudo: required # 超级权限
dist: trusty # 在ubuntu:trusty
language: go # 声明构建语言环境
go: # 只构建最新版本
- 1.8
services: # docker环境
- docker
branches: # 限定项目分支
only:
- /^v[0-9](\.[0-9]){2}(-rc[1-9])?$/
install:
- curl https://glide.sh/get | sh # 安装glide包管理
script:
- glide up
- GOOS=linux GOARCH=amd64 go build # 编译版本
- 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
- docker push registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
before_deploy:
- ./dist.sh
deploy:
provider: releases
api_key:
secure: AGW05sQuQfjy77+JprSV+ohti/VVgFuh7UOTV0+hwxqsOVXSoIQz/ZPOlHWPP1iiSiGGEalspm+UtKRvADcDfllUaEwo7kebfFeMx4X//qxFxQSQ5LJYx7qxsTDpuQ4CF8zifCtND3ynnUAdx0P6FFkxE/67kN2n4CrhIxYCUb8gNPzDDRuS0ZNBC4zzNldJo/vtatbvc2btuFfwKoClYf+xPLy5luLqDvKF+hdjJ8NuZl8BWkWxXE+kk8fW4iUn2IV0qtLRZ3FQUyAF2CumzxqZfViX+rYTXsfbabYY5nYG6opT4mUEF58T4X3uRV0e3Q6Fe73nmLh9cyAoQl1BTSJ1XiyV4eJJWcKEMY7DqJ646lzoUT449YwvTK57klcfBbShpcjFf2alVdEbr9jbEXrCkuWKnssO9VfufhYF6t9h22c79evpexpIbsoncPD+b+n712MzufREtUF4kpUdkIir5n9CgQl/l7S+fV+n+gME+mcA44K7iPXkC80UfxJiw83QizT39OQhExq6SPIwrbt2vlAkBpSLMUS9iAHtTJYUsmH1SsmrxGK3WromKysWeTRJbcAJls2k6V313sn4TuYBWiHTUfsUBhv+objDFA2TsfO+g0g1JsdfZb5EsKrqNvs/2ta1xlzdE0+/TLG/YNKIOPkHnXswAM3DZm3zEss=
skip_cleanup: true
file: "*.tar.gz"
file_glob: true
on:
tags: true
repo: eiblog/eiblog
all_branches: true

View File

@@ -1,11 +1,105 @@
# Eiblog Changelog
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.4.9](https://github.com/eiblog/eiblog/compare/v1.4.8...v1.4.9) (2019-12-18)
### Bug Fixes
* **disqus:** connect reset by peer ([1bdfb6a](https://github.com/eiblog/eiblog/commit/1bdfb6a))
### v1.4.4 (2018-05-07)
* 修复基础评论分钟数计算错误
* let's encrypt v2证书内嵌ct故移除有关ct内容
### v1.4.3 (2018-02-09)
* 修复博客初始化后about 页面不能够评论 #6
* 修复编辑专题,按钮显示“添加专题”错误
* 优化“添加文章”从同步改为异步推送feedesdisqus。速度显著提升
* **重要*)头像图片从 avatar.jpg 改为 avatar.png透明
* docker-compose.yml mongodb 去掉端口映射,防止用户将端口暴露至外网
* session key 每次重启随机生成等一些细节的修复
### v1.4.2 (2018-01-25)
* fix archive page bug
### v1.4.1 (2018-01-14)
* 修复创建新文章disqus 不收录bug
* 修复创建新文章归档页面不刷新bug
* 修复能够删除关于页面和友情链接页面bug
* 修复重复添加文章错误
* 注释掉 docker-compose.yml 自动备份内容,请自行解开
* 添加当月数大于12归档页面使用年份归档
* 优化代码逻辑
### v1.4.0 (2018-01-01)
* fix 搜索页面 bug
* CGO_ENABLED=0 关闭 cgo
* 更新Makefile ct log 服务器
* 数据库数据终于可以备份了
### v1.3.4 (2017-11-29)
* fix page:admin/write-post autocomplete tag
### v1.3.3 (2017-11-27)
* fix docker image: exec user process caused "no such file or directory"
### v1.3.2 (2017-11-17)
* 修复文章自动保存引起的发布文章不成功的bug
### v1.3.1 (2017-11-05)
* 修复调整 关于、友情链接 创建时间出现文章乱序
* 修复评论时间计算错误
* 调整acme文件验证路径
* 更改七牛SDK包为github包。
* 调整七牛配置文件名称app.yml: kodo -> qiniuname -> bucket请提高静态文件版本 staticversion
### v1.3.0 (2017-07-13)
* 更改 app.yml 配置项,将大部分配置归在 general 常规配置下。注意,部署时请先更新 app.yml。
* 静态文件采用动态渲染,即用户不再需要管理 view、static 目录。
* 通过 acme.sh 使用双证书啦,可到 Makefile 查看相关信息。
* 使用 autocert 自动生成证书功能,从此再也不用担心证书过期,移步 [证书更新](https://github.com/eiblog/eiblog/blob/master/docs/autocert.md)。
* 开启配置项 enablehttps 将自动重定向 http 到 https 啦。
* disqus.js 文件由配置指定,请看 app.yml 下的 disqus 相关配置。
### 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)
## v1.1.1 (2017-02-07)
* 添加文章描述功能。
* 修复评论`jQuery`文件引用错误。
* 修复`.travis.yml`描述错误。
## v1.0.0 (2016-01-09)
### v1.0.0 (2016-01-09)
首次发布版本
* 全站`HTTPS`设计,安全、极速。

View File

@@ -1,11 +1,10 @@
FROM alpine
MAINTAINER deepzz <deepzz.qi@gmail.com>
RUN apk update
RUN apk add ca-certificates
RUN apk add --update --no-cache ca-certificates
ADD static/tzdata/Shanghai /etc/localtime
COPY . /eiblog
EXPOSE 9000
WORKDIR /eiblog
ENTRYPOINT ["./eiblog"]
CMD ["sh","-c","/eiblog/eiblog"]

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014 Manuel Martínez-Almeida
Copyright (c) 2017 deepzz deepzz.qi@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

73
Makefile Normal file
View File

@@ -0,0 +1,73 @@
.PHONY: test build deploy dist gencert dhparams ssticket makedir clean
# use aliyun dns api to auto renew cert.
# env:
# export Ali_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
# export Ali_Secret="jlsdflanljkljlfdsaklkjflsa"
docker_registry?=registry.cn-hangzhou.aliyuncs.com
acme?=~/.acme.sh
acme.sh?=$(acme)/acme.sh
config?=/data/eiblog/conf
test:
mongodb:
@if ! docker ps | grep mongodb; then \
docker run -d --name mongodb -v mongo-data:/data/db -p 27018:27017 mongo:3.2; \
fi
run: mongodb
@echo "run eiblog..."
@go build && ./eiblog
build:
@echo "go build..."
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build && \
docker build -t $(docker_registry)/deepzz/eiblog:latest .
deploy:build
@docker push $(docker_registry)/deepzz/eiblog:latest
dist:
@./dist.sh
gencert:makedir
@if [ ! -n "$(sans)" ]; then \
printf "Need one argument [sans=params]\n"; \
printf "example: sans=\"-d domain -d *.domain\"\n"; \
exit 1; \
fi; \
if [ ! -n "$(cn)" ]; then \
printf "Need one argument [cn=params]\n"; \
printf "example: cn=domain\n"; \
exit 1; \
fi
@if [ ! -f $(acme.sh) ]; then \
curl https://get.acme.sh | sh; \
fi
@echo "generate rsa cert..."
@$(acme.sh) --force --issue --dns dns_ali $(sans) \
--renew-hook "$(acme.sh) --install-cert -d $(cn) \
--key-file $(config)/ssl/domain.rsa.key \
--fullchain-file $(config)/ssl/domain.rsa.pem \
--reloadcmd \"service nginx force-reload\""
@echo "generate ecc cert..."
@$(acme.sh) --force --issue --dns dns_ali $(sans) -k ec-256 \
--renew-hook "$(acme.sh) --install-cert -d $(cn) --ecc \
--key-file $(config)/ssl/domain.ecc.key \
--fullchain-file $(config)/ssl/domain.ecc.pem \
--reloadcmd \"service nginx force-reload\""
dhparams:
@openssl dhparam -out $(config)/ssl/dhparams.pem 2048
ssticket:
@openssl rand 48 > $(config)/ssl/session_ticket.key
makedir:
@mkdir -p $(config)/ssl
clean:

View File

@@ -1,8 +1,8 @@
# 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)
# EiBlog [![Build Status](https://travis-ci.org/eiblog/eiblog.svg?branch=v1.3.0)](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的很大帮助在此表示感谢。
用过其它博客系统,不喜欢,不够轻,不够快!自己做过共两款博客系统,完美主义的我(毕竟处女座)也实在是不想再在这件事情上过多纠结了。`Eiblog`应该是一个比较稳定的博客系统,且是博主以后使用的博客系统,稳定性和维护你是不用担心的,唯独该系统部署过程太过复杂,并且不推荐没有计算机知识的朋友搭建,欢迎咨询。该博客的个中优点(明显快,安全),等你体验。
用过其它博客系统,不喜欢,不够轻,不够快!自己做过共两款博客系统,完美主义的我(毕竟处女座)也实在是不想再在这件事情上过多纠结了。`Eiblog` 应该是一个比较稳定的博客系统,且是博主以后使用的博客系统,稳定性和维护你是不用担心的,唯独该系统部署过程太过复杂,并且不推荐没有计算机知识的朋友搭建,欢迎咨询。该博客的个中优点(明显快,安全),等你体验。
<!--more-->
@@ -10,76 +10,85 @@
整个博客系统涉及到模块如下:
* 自动更新证书:
* 接入 [acme/autocert](https://github.com/golang/crypto/tree/master/acme/autocert),在 TLS 层开启全自动更新证书,从此证书的更新再也不用惦记了,不过 Go 的 HTTPS 兼容性不够好(不想兼容),在如部分 IE 和 UC 之类的浏览器不能访问,请悉知。
* 如果你采用如 Nginx 代理,推荐使用 [acme.sh](https://github.com/Neilpang/acme.sh) 实现证书的自动部署。博主实现 aliyun dns 的自动验证方式,详见 [Makefile/gencert](https://github.com/eiblog/eiblog/blob/master/Makefile)。
* `MongoDB`,博客采用 mongodb 作为存储数据库。
* `Elasticsearch`,采用`elasticsearch`作为博客的站内搜索,尽管占用内存稍高。
* `Elasticsearch`,采用 `elasticsearch` 作为博客的站内搜索,尽管占用内存稍高。
* `Disqus`,作为博客评论系统,国内大部分被墙,故实现两种评论方式。
* `Nginx`,作为反向代理服务器,并做相关`http header`和证书的设置。
* `Nginx`,作为反向代理服务器,并做相关 `http header` 和证书的设置。
* `Google Analytics`,作为博客系统的数据分析统计工具。
* `七牛 CDN`,作为博客系统的静态文件存储,博文的图片附件什么上传至这里。
### 图片展示
可以容易的看到[httpsecurityreport](https://httpsecurityreport.com/?report=deepzz.com)评分`96`[ssllabs](https://www.ssllabs.com/ssltest/analyze.html?d=deepzz.com&latest)评分`A+`,堪称完美。这些安全的相关配置会在后面的部署过程中接触到。
可以容易的看到 [httpsecurityreport](https://httpsecurityreport.com/?report=deepzz.com) 评分`96`[ssllabs](https://www.ssllabs.com/ssltest/analyze.html?d=deepzz.com&latest) 评分`A+`[myssl](https://myssl.com/deepzz.com) 评分`A+`,堪称完美。这些安全的相关配置会在后面的部署过程中接触到。
相关图片展示:
![show-home](http://7xokm2.com1.z0.glb.clouddn.com/static/img/show-home1.png)
![show-home](http://gb.st.deepzz.com/static/img/show-home1.png)
![show-home2](http://7xokm2.com1.z0.glb.clouddn.com/static/img/show-home2.png)
![show-home2](http://gb.st.deepzz.com/static/img/show-home2.png)
![show-admin](http://7xokm2.com1.z0.glb.clouddn.com/static/img/show-admin.png)
![show-admin](http://gb.st.deepzz.com/static/img/show-admin.png)
![eiblog-mem](http://7xokm2.com1.z0.glb.clouddn.com/img/eiblog-mem.png)
![eiblog-mem](http://gb.st.deepzz.com/img/eiblog-mem.png)
> `注`图片1图片2是博客界面图片3是后台界面图片4是性能展示
> `注`图片1图片2是博客界面图片3是后台界面图片4是内存占用
### 极速体验
1. 到[这里](https://github.com/eiblog/eiblog/releases)下载对应平台`.tar.gz`文件
`eiblog` 默认监听 `:9000` 端口,默认连接 `MongoDB` 地址 `mongodb:27017`,默认连接 `Elasticsearch` 地址 `http://elasticsearch:9200`
2. 搭建`MongoDB`(必须)和`Elasticsearch`(可选)服务。
1、手动启动执行
3. 修改`/etc/hosts`文件,添加`MongoDB`数据库 IP 地址,如:`127.0.0.1 eidb`
1. 到 [这里](https://github.com/eiblog/eiblog/releases) 下载对应平台 `.tar.gz` 文件
2. 搭建 `MongoDB`(必须)和 `Elasticsearch`(可选)服务,正式部署需要。
3. `MongoDB` 服务地址也可通过环境变量指定连接地址如:`export EIBLOG_MGO_ADDR=127.0.0.1:27017`
4. 执行 `./eiblog`,运行博客系统。看到 `...Listening and serving HTTP on :9000` 代表运行成功了。
4. 执行`./eiblog`,运行博客系统。看到:
默认监听 `HTTP 9000` 端口,后台 `/admin/login`,默认账号密码均为 `deepzz`。更多详细请查阅 [安装部署](https://github.com/eiblog/eiblog/blob/master/docs/install.md) 文档。
2、Docker 与 Go 环境
如果你有 `docker``go` 环境,可直接使用如下命令启动:
```
...
...
[GIN-debug] Listening and serving HTTP on :9000
$ EIBLOG_MGO_ADDR=127.0.0.1:27017 make run
```
代表运行成功了
默认监听`9000`端口,后台`/admin/login`,默认账号密码均为`deepzz`。更多详细请查阅[安装部署](https://github.com/eiblog/eiblog/blob/master/docs/install.md)文档。
请提前指定 `mongodb` 的连接地址。该命令会启动一个 `mognodb` 容器,然后编译 `eiblog` 并运行
### 特色功能
作为博主之心血之作,`Eiblog`实现了什么功能,有什么特点,做了什么优化呢?
作为博主之心血之作,`Eiblog` 实现了什么功能,有什么特点,做了什么优化呢?
1. 系统目前只有`首页``专题``归档``友链``关于``搜索`界面。相信已经可以满足大部分用户的需求。
2. `.js``.css`等静态文件本地存储,小图片 base64 内置到 css 中,不会产生网络所带来的延迟,加速网页访问。版本控制方式,动态更新静态文件。
1. 系统目前只有 `首页``专题``归档``友链``关于``搜索` 界面。相信已经可以满足大部分用户的需求。
2. `.js``.css` 等静态文件本地存储,小图片 base64 内置到 css 中,不会产生网络所带来的延迟,加速网页访问。版本控制方式,动态更新静态文件。
3. 采用谷歌统计,并实现异步(将访问信息发给后端,后端提交给谷歌)统计,加速访问速度。
4. 采用直接缓存 markdown 转过的 html 文档的方式,加速后端处理。响应速度均在 3ms 以内,真正极速。
5. 通过 Nginx 的配置,开启压缩缩小传输量,服务器传输证书链、开启`Session Resumption``Session Ticket``OCSP Stapling`等加速证书握手,再次提高速度。
* `CDN`使用七牛融合CDN`https`化,实现全站`https`。七牛可申请免费证书了。
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被证实是不安全的。
 * `HPKP`HTTP 公钥固定扩展,防范由「伪造或不正当手段获得网站证书」造成的中间人攻击。该功能让我们选择信任哪些`CA`请不要轻易尝试 Nginx 线上运行,因为该配置目前只指定了 Letsencrypt X3 和 TrustAsia G5 证书 pin-sha256。
 * `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`功能,搜索更加自然。
7. 针对 `disqus` 被墙原因,实现 [Jerry Qu](https://imququ.com) 的另类评论方式,保证评论的流畅。
8. 开源 `Typecho` 完整后台系统,全功能 `markdown` 编辑器,让你体验什么是简洁清爽。
9. 博客后台直接对接 `七牛 SDK`,实现后台上传文件和删除文件的简单功能。
10. 采用 `Elasticsearch` 作为站内搜索,结合 `google opensearch` 功能,搜索更加自然。
11. 自动备份数据库数据到七牛云。
### 文档
* [证书更新](https://github.com/eiblog/eiblog/blob/master/docs/autocert.md)
* [安装部署](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)
* [关于备份](https://github.com/eiblog/backup)
### 成功搭建者博客
* [https://razeencheng.com/](https://razeencheng.com/) - Razeen's Blog
* [https://blog.netcj.com](https://blog.netcj.com) - Razeen's Blog
如果你的博客使用`Eiblog`搭建,你可以在[这里](https://github.com/eiblog/eiblog/issues/1)提交网址。
如果你的博客使用`Eiblog`搭建,你可以在 [这里](https://github.com/eiblog/eiblog/issues/1) 提交网址。

207
api.go
View File

@@ -4,23 +4,26 @@ import (
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
"github.com/eiblog/utils/mgo"
"github.com/gin-gonic/gin"
"gopkg.in/mgo.v2/bson"
)
const (
// 成功
NOTICE_SUCCESS = "success"
NOTICE_NOTICE = "notice"
NOTICE_ERROR = "error"
// 注意
NOTICE_NOTICE = "notice"
// 错误
NOTICE_ERROR = "error"
)
// 全局 API
var APIs = make(map[string]func(c *gin.Context))
func init() {
@@ -52,6 +55,7 @@ func init() {
APIs["file-delete"] = apiFileDelete
}
// 更新账号信息Email、PhoneNumber、Address
func apiAccount(c *gin.Context) {
e := c.PostForm("email")
pn := c.PostForm("phoneNumber")
@@ -61,8 +65,10 @@ func apiAccount(c *gin.Context) {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
return
}
err := UpdateAccountField(bson.M{"$set": bson.M{"email": e, "phonen": pn, "address": ad}})
err := UpdateAccountField(mgo.M{"$set": mgo.M{"email": e, "phonen": pn, "address": ad}})
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
@@ -72,6 +78,7 @@ func apiAccount(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "更新成功", "")
}
// 更新博客信息
func apiBlog(c *gin.Context) {
bn := c.PostForm("blogName")
bt := c.PostForm("bTitle")
@@ -83,8 +90,12 @@ func apiBlog(c *gin.Context) {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
return
}
err := UpdateAccountField(bson.M{"$set": bson.M{"blogger.blogname": bn, "blogger.btitle": bt, "blogger.beian": ba, "blogger.subtitle": st, "blogger.seriessay": ss, "blogger.archivessay": as}})
err := UpdateAccountField(mgo.M{"$set": mgo.M{"blogger.blogname": bn,
"blogger.btitle": bt, "blogger.beian": ba, "blogger.subtitle": st,
"blogger.seriessay": ss, "blogger.archivessay": as}})
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
@@ -99,6 +110,7 @@ func apiBlog(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "更新成功", "")
}
// 更新密码
func apiPassword(c *gin.Context) {
logd.Debug(c.Request.PostForm.Encode())
od := c.PostForm("old")
@@ -117,8 +129,10 @@ func apiPassword(c *gin.Context) {
return
}
newPwd := EncryptPasswd(Ei.Username, nw)
err := UpdateAccountField(bson.M{"$set": bson.M{"password": newPwd}})
err := UpdateAccountField(mgo.M{"$set": mgo.M{"password": newPwd}})
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
@@ -126,45 +140,39 @@ func apiPassword(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "更新成功", "")
}
// 删除文章,软删除:移入到回收箱
func apiPostDelete(c *gin.Context) {
var err error
defer func() {
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}()
err = c.Request.ParseForm()
if err != nil {
return
}
var ids []int32
var i int
for _, v := range c.Request.PostForm["cid[]"] {
i, err = strconv.Atoi(v)
if err != nil || i < 1 {
err = errors.New("参数错误")
for _, v := range c.PostFormArray("cid[]") {
i, err := strconv.Atoi(v)
if err != nil || int32(i) < setting.Conf.General.StartID {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
return
}
ids = append(ids, int32(i))
}
err = DelArticles(ids...)
err := DelArticles(ids...)
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
// elasticsearch 删除索引
// elasticsearch
err = ElasticDelIndex(ids)
if err != nil {
return
logd.Error(err)
}
// TODO disqus delete
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}
func apiPostAdd(c *gin.Context) {
var err error
var do string
var cid int
var (
err error
do string
cid int
)
defer func() {
switch do {
case "auto": // 自动保存
@@ -173,29 +181,28 @@ func apiPostAdd(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{"success": SUCCESS, "time": time.Now().Format("15:04:05 PM"), "cid": cid})
case "save": // 保存草稿
case "save", "publish": // 草稿,发布
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
c.Redirect(http.StatusFound, "/admin/manage-draft")
case "publish": // 发布
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
uri := "/admin/manage-draft"
if do == "publish" {
uri = "/admin/manage-posts"
}
c.Redirect(http.StatusFound, "/admin/manage-posts")
c.Redirect(http.StatusFound, uri)
}
}()
do = c.PostForm("do") // auto or save or publish
slug := c.PostForm("slug")
title := c.PostForm("title")
text := c.PostForm("text")
date := c.PostForm("date")
date := CheckDate(c.PostForm("date"))
serie := c.PostForm("serie")
tag := c.PostForm("tags")
update := c.PostForm("update")
if title == "" || text == "" || slug == "" {
if slug == "" || title == "" || text == "" {
err = errors.New("参数错误")
return
}
@@ -208,13 +215,14 @@ func apiPostAdd(c *gin.Context) {
Title: title,
Content: text,
Slug: slug,
CreateTime: CheckDate(date),
CreateTime: date,
IsDraft: do != "publish",
Author: Ei.Username,
SerieID: serieid,
Tags: tags,
}
cid, err = strconv.Atoi(c.PostForm("cid"))
// 新文章
if err != nil || cid < 1 {
err = AddArticle(artc)
if err != nil {
@@ -223,58 +231,55 @@ func apiPostAdd(c *gin.Context) {
}
cid = int(artc.ID)
if !artc.IsDraft {
ElasticIndex(artc)
DoPings(slug)
// 异步执行,快
go func() {
// elastic
ElasticIndex(artc)
// rss
DoPings(slug)
// disqus
ThreadCreate(artc)
}()
}
return
}
// 旧文章
artc.ID = int32(cid)
i, a := GetArticle(artc.ID)
_, a := GetArticle(artc.ID)
if a != nil {
artc.IsDraft = false
artc.Count = a.Count
artc.UpdateTime = a.UpdateTime
Ei.Articles = append(Ei.Articles[0:i], Ei.Articles[i+1:]...)
DelFromLinkedList(a)
ManageTagsArticle(a, false, DELETE)
ManageSeriesArticle(a, false, DELETE)
ManageArchivesArticle(a, false, DELETE)
delete(Ei.MapArticles, a.Slug)
a = nil
}
if CheckBool(update) {
artc.UpdateTime = time.Now()
}
err = UpdateArticle(bson.M{"id": artc.ID}, artc)
// 数据库更新
err = UpdateArticle(mgo.M{"id": artc.ID}, artc)
if err != nil {
logd.Error(err)
return
}
if !artc.IsDraft {
Ei.MapArticles[artc.Slug] = artc
Ei.Articles = append(Ei.Articles, artc)
sort.Sort(Ei.Articles)
GenerateExcerptAndRender(artc)
// elasticsearch 索引
ElasticIndex(artc)
DoPings(slug)
if artc.ID >= setting.Conf.StartID {
ManageTagsArticle(artc, true, ADD)
ManageSeriesArticle(artc, true, ADD)
ManageArchivesArticle(artc, true, ADD)
AddToLinkedList(artc.ID)
}
ReplaceArticle(a, artc)
// 异步执行,快
go func() {
// elastic
ElasticIndex(artc)
// rss
DoPings(slug)
// disqus
if a == nil {
ThreadCreate(artc)
}
}()
}
}
// 只能逐一删除,专题下不能有文章
func apiSerieDelete(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
// 只能逐一删除
for _, v := range c.Request.PostForm["mid[]"] {
for _, v := range c.PostFormArray("mid[]") {
id, err := strconv.Atoi(v)
if err != nil || id < 1 {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
@@ -290,6 +295,7 @@ func apiSerieDelete(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}
// 添加专题,如果专题有提交 mid 即更新专题
func apiSerieAdd(c *gin.Context) {
name := c.PostForm("name")
slug := c.PostForm("slug")
@@ -326,24 +332,15 @@ func apiSerieAdd(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "操作成功", "")
}
// 暂未启用
// NOTE 排序专题,暂未实现
func apiSerieSort(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
v := c.Request.PostForm["mid[]"]
v := c.PostFormArray("mid[]")
logd.Debug(v)
}
// 删除草稿箱,物理删除
func apiDraftDelete(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
for _, v := range c.Request.PostForm["mid[]"] {
for _, v := range c.PostFormArray("mid[]") {
i, err := strconv.Atoi(v)
if err != nil || i < 1 {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
@@ -358,15 +355,9 @@ func apiDraftDelete(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}
// 删除垃圾箱,物理删除
func apiTrashDelete(c *gin.Context) {
logd.Debug(c.PostForm("key"))
logd.Debug(c.Request.PostForm)
err := c.Request.ParseForm()
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
for _, v := range c.Request.PostForm["mid[]"] {
for _, v := range c.PostFormArray("mid[]") {
i, err := strconv.Atoi(v)
if err != nil || i < 1 {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
@@ -381,15 +372,9 @@ func apiTrashDelete(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}
// 从垃圾箱恢复到草稿箱
func apiTrashRecover(c *gin.Context) {
logd.Debug(c.PostForm("key"))
logd.Debug(c.Request.PostForm)
err := c.Request.ParseForm()
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
for _, v := range c.Request.PostForm["mid[]"] {
for _, v := range c.PostFormArray("mid[]") {
i, err := strconv.Atoi(v)
if err != nil || i < 1 {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
@@ -405,6 +390,7 @@ func apiTrashRecover(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "恢复成功", "")
}
// 上传文件到 qiniu 云
func apiFileUpload(c *gin.Context) {
type Size interface {
Size() int64
@@ -428,7 +414,7 @@ func apiFileUpload(c *gin.Context) {
c.String(http.StatusBadRequest, err.Error())
return
}
typ := c.Request.Header.Get("Content-Type")
typ := header.Header.Get("Content-Type")
c.JSON(http.StatusOK, gin.H{
"title": filename,
"isImage": typ[:5] == "image",
@@ -437,20 +423,19 @@ func apiFileUpload(c *gin.Context) {
})
}
// 删除七牛 CDN 文件
func apiFileDelete(c *gin.Context) {
var err error
defer func() {
if err != nil {
logd.Error(err)
}
c.String(http.StatusOK, "删掉了吗?鬼知道。。。")
}()
defer c.String(http.StatusOK, "删掉了吗?鬼知道。。。")
name := c.PostForm("title")
if name == "" {
err = errors.New("参数错误")
logd.Error("参数错误")
return
}
err = FileDelete(name)
err := FileDelete(name)
if err != nil {
logd.Error(err)
}
}
func responseNotice(c *gin.Context, typ, content, hl string) {

20
back.go
View File

@@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"net/http"
@@ -11,11 +12,12 @@ import (
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
"github.com/eiblog/utils/mgo"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"gopkg.in/mgo.v2/bson"
)
// 是否登录
func isLogin(c *gin.Context) bool {
session := sessions.Default(c)
v := session.Get("username")
@@ -25,6 +27,7 @@ func isLogin(c *gin.Context) bool {
return true
}
// 登陆过滤
func AuthFilter() gin.HandlerFunc {
return func(c *gin.Context) {
if !isLogin(c) {
@@ -51,6 +54,7 @@ func HandleLogin(c *gin.Context) {
RenderHTMLBack(c, "login.html", gin.H{"BTitle": Ei.BTitle})
}
// 登陆接口
func HandleLoginPost(c *gin.Context) {
user := c.PostForm("user")
pwd := c.PostForm("password")
@@ -61,7 +65,7 @@ func HandleLoginPost(c *gin.Context) {
return
}
if Ei.Username != user || !VerifyPasswd(Ei.Password, user, pwd) {
logd.Printf("账号或密码错误 %s, %s", user, pwd)
logd.Printf("账号或密码错误 %s, %s\n", user, pwd)
c.Redirect(http.StatusFound, "/admin/login")
return
}
@@ -70,12 +74,12 @@ func HandleLoginPost(c *gin.Context) {
session.Save()
Ei.LoginIP = c.ClientIP()
Ei.LoginTime = time.Now()
UpdateAccountField(bson.M{"$set": bson.M{"loginip": Ei.LoginIP, "logintime": Ei.LoginTime}})
UpdateAccountField(mgo.M{"$set": mgo.M{"loginip": Ei.LoginIP, "logintime": Ei.LoginTime}})
c.Redirect(http.StatusFound, "/admin/profile")
}
func GetBack() gin.H {
return gin.H{"Author": Ei.Username, "Kodo": setting.Conf.Kodo}
return gin.H{"Author": Ei.Username, "Qiniu": setting.Conf.Qiniu}
}
// 个人配置
@@ -115,11 +119,13 @@ func HandlePost(c *gin.Context) {
for tag, _ := range Ei.Tags {
tags = append(tags, T{tag, tag})
}
h["Tags"] = tags
str, _ := json.Marshal(tags)
h["Tags"] = string(str)
c.Status(http.StatusOK)
RenderHTMLBack(c, "admin-post", h)
}
// 删除草稿
func HandleDraftDelete(c *gin.Context) {
id, err := strconv.Atoi(c.Query("cid"))
if err != nil || id < 1 {
@@ -154,7 +160,7 @@ func HandlePosts(c *gin.Context) {
h["Serie"] = se
h["KW"] = kw
var max int
max, h["List"] = PageListBack(se, kw, false, false, pg, setting.Conf.PageSize)
max, h["List"] = PageListBack(se, kw, false, false, pg, setting.Conf.General.PageSize)
if pg < max {
vals.Set("page", fmt.Sprint(pg+1))
h["Next"] = vals.Encode()
@@ -184,6 +190,7 @@ func HandleSeries(c *gin.Context) {
RenderHTMLBack(c, "admin-series", h)
}
// 编辑专题
func HandleSerie(c *gin.Context) {
h := GetBack()
id, err := strconv.Atoi(c.Query("mid"))
@@ -276,6 +283,7 @@ func HandleAPI(c *gin.Context) {
api(c)
}
// 渲染 html
func RenderHTMLBack(c *gin.Context, name string, data gin.H) {
if name == "login.html" {
err := Tmpl.ExecuteTemplate(c.Writer, name, data)

View File

@@ -1,10 +0,0 @@
#!/bin/bash
echo "go build..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build && \
domain="registry.cn-hangzhou.aliyuncs.com" && \
docker build -t $domain/deepzz/eiblog . && \
read -p "是否上传到服务器(y/n):" word && \
if [ $word = "y" ] ;then
docker push $domain/deepzz/eiblog
fi

View File

@@ -6,25 +6,30 @@ import (
"time"
)
// 检查 email
func CheckEmail(e string) bool {
reg := regexp.MustCompile(`^(\w)+([\.\-]\w+)*@(\w)+((\.\w+)+)$`)
return reg.MatchString(e)
}
// 检查 domain
func CheckDomain(domain string) bool {
reg := regexp.MustCompile(`^(http://|https://)?[0-9a-zA-Z]+[0-9a-zA-Z\.-]*\.[a-zA-Z]{2,4}$`)
return reg.MatchString(domain)
}
// 检查 sms
func CheckSMS(sms string) bool {
reg := regexp.MustCompile(`^\+\d+$`)
return reg.MatchString(sms)
}
// 检查 password
func CheckPwd(pwd string) bool {
return len(pwd) > 5 && len(pwd) < 19
}
// 检查日期
func CheckDate(date string) time.Time {
if t, err := time.ParseInLocation("2006-01-02 15:04", date, time.Local); err == nil {
return t
@@ -32,6 +37,7 @@ func CheckDate(date string) time.Time {
return time.Now()
}
// 检查 id
func CheckSerieID(sid string) int32 {
if id, err := strconv.Atoi(sid); err == nil {
return int32(id)
@@ -39,6 +45,7 @@ func CheckSerieID(sid string) int32 {
return 0
}
// bool
func CheckBool(str string) bool {
return str == "true" || str == "1"
}

View File

@@ -2,28 +2,40 @@ package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCheckEmail(t *testing.T) {
e := "xx@email.com"
e1 := "xxxxemail.com"
e2 := "xxx#email.com"
emails := []string{
"xx@email.com",
"xxxxemail.com",
"xxx#email.com",
}
t.Log(CheckEmail(e))
t.Log(CheckEmail(e1))
t.Log(CheckEmail(e2))
for i, v := range emails {
if i == 0 {
assert.True(t, CheckEmail(v))
} else {
assert.False(t, CheckEmail(v))
}
}
}
func TestCheckDomain(t *testing.T) {
d := "123.com"
d1 := "http://123.com"
d2 := "https://123.com"
d3 := "123#.com"
d4 := "123.coooom"
domains := []string{
"123.com",
"http://123.com",
"https://123.com",
"123#.com",
"123.coooom",
}
t.Log(CheckDomain(d))
t.Log(CheckDomain(d1))
t.Log(CheckDomain(d2))
t.Log(CheckDomain(d3))
t.Log(CheckDomain(d4))
for i, v := range domains {
if i > 2 {
assert.False(t, CheckDomain(v))
} else {
assert.True(t, CheckDomain(v))
}
}
}

View File

@@ -1,73 +1,82 @@
# 运行模式 dev or prod
runmode: dev
# 回收箱保留48小时
trash: -48
# 定时清理回收箱,%d小时
clean: 1
# 首页展示文章数量
pagenum: 10
# 管理界面
pagesize: 20
# 自动截取预览, 字符数
length: 400
# 截取预览标识
identifier: <!--more-->
# 文章描述前缀
description: "Desc:"
# 起始ID预留id不时之需, 不用管
startid: 11
# elasticsearch url
searchurl: http://elasticsearch:9200
# 评论相关
disqus:
shortname: deepzz
publickey: wdSgxRm9rdGAlLKFcFdToBe3GT4SibmV7Y8EjJQ0r4GWXeKtxpopMAeIeoI2dTEg
postscount: https://disqus.com/api/3.0/threads/set.json
postslist: https://disqus.com/api/3.0/threads/listPosts.json
postcreate: https://disqus.com/api/3.0/posts/create.json
interval: 5
# 静态文件版本
staticversion: 1
# superfeedr url
feedrurl: https://deepzz.superfeedr.com/
# 热搜词配置
hotwords:
- docker
# 谷歌统计
google:
tid: UA-77251712-1
v: "1"
t: pageview
# 静态文件版本
staticversion: 1
# 七牛CDN
kodo:
name: eiblog
domain: st.deepzz.com
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
# 运行模式
mode:
# 默认开启HTTP
enablehttp: true
httpport: 9000
enablehttps: false
httpsport: 443
certfile: conf/certs/domain.pem
keyfile: conf/certs/domain.key
domain: deepzz.com
# twitter地址: twitter.com/chenqijing2
twitter:
card: summary
site: chenqijing2
image: st.deepzz.com/static/img/avatar.jpg
address: twitter.com/chenqijing2
# superfeedr url
feedrurl: https://deepzz.superfeedr.com/
- mongodb
- curl
- dns
# ping rpcs 地址
pingrpcs:
- http://ping.baidu.com/ping/RPC2
- http://blogsearch.google.com/ping/RPC2
- http://rpc.pingomatic.com/
# 常规配置
general:
# 首页展示文章数量
pagenum: 10
# 管理界面
pagesize: 20
# 起始ID预留id不时之需, 不用管
startid: 11
# 文章描述前缀
descprefix: "Desc:"
# 截取预览标识
identifier: <!--more-->
# 自动截取预览, 字符数
length: 400
# 回收箱保留48小时
trash: -48
# 定时清理回收箱,每 %d 小时
clean: 1
# 评论相关
disqus:
shortname: xxxxxx
publickey: wdSgxRm9rdGAlLKFcFdToBe3GT4SibmV7Y8EjJQ0r4GWXeKtxpopMAeIeoI2dTEg
accesstoken: 50023908f39f4607957e909b495326af
postscount: https://disqus.com/api/3.0/threads/set.json
postslist: https://disqus.com/api/3.0/threads/listPosts.json
postcreate: https://disqus.com/api/3.0/posts/create.json
postapprove: https://disqus.com/api/3.0/posts/approve.json
threadcreate: https://disqus.com/api/3.0/threads/create.json
# 获取评论数量间隔
interval: 5
# 谷歌统计
google:
url: https://www.google-analytics.com/collect
tid: UA-xxxxxx-1
v: "1"
t: pageview
# 七牛CDN
qiniu:
bucket: eiblog
domain: st.deepzz.com
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
# 运行模式
mode:
# http server
enablehttp: true
httpport: 9000
# https server
enablehttps: false
autocert: false
httpsport: 9001
certfile: conf/ssl/domain.rsa.pem
keyfile: conf/ssl/domain.rsa.key
domain: deepzz.com
# twitter地址: twitter.com/chenqijing2
twitter:
card: summary
site: deepzz02
image: st.deepzz.com/static/img/avatar.jpg
address: twitter.com/deepzz02
# 以下数据项供初始化使用,仅首次运行有效。
# 若需要修改,请到博客后台操作。
# 数据初始化操作,可到博客后台修改
account:
# *后台登录用户名
username: deepzz

View File

@@ -0,0 +1 @@
# like 192.168.99.100:true

View File

@@ -1,157 +0,0 @@
server {
listen 443 ssl http2 fastopen=3 reuseport;
server_name www.deepzz.com deepzz.com;
server_tokens off;
include /data/eiblog/conf/nginx/ip.blacklist;
# 现在一般证书是内置的。可以注释该项
# https://imququ.com/post/certificate-transparency.html#toc-2
# ssl_ct on;
# ssl_ct_static_scts /data/eiblog/conf/scts;
# 中间证书 + 站点证书
ssl_certificate /data/eiblog/conf/ssl/domain.pem;
# 创建 CSR 文件时用的密钥
ssl_certificate_key /data/eiblog/conf/ssl/domain.key;
# openssl dhparam -out dhparams.pem 2048
# https://weakdh.org/sysadmin.html
ssl_dhparam /data/eiblog/conf/ssl/dhparams.pem;
# https://github.com/cloudflare/sslconfig/blob/master/conf
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
# 如果启用了 RSA + ECDSA 双证书Cipher Suite 可以参考以下配置:
# ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets on;
# openssl rand 48 > session_ticket.key
# 单机部署可以不指定 ssl_session_ticket_key
# ssl_session_ticket_key /data/eiblog/conf/ssl/session_ticket.key;
ssl_stapling on;
ssl_stapling_verify on;
# 根证书 + 中间证书
# https://imququ.com/post/why-can-not-turn-on-ocsp-stapling.html
ssl_trusted_certificate /data/eiblog/conf/ssl/full_chained.pem;
resolver 114.114.114.114 8.8.8.8 valid=300s;
resolver_timeout 10s;
access_log /data/eiblog/logdata/nginx.log;
if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) {
return 444;
}
if ($host != 'deepzz.com' ) {
rewrite ^/(.*)$ https://deepzz.com/$1 permanent;
}
# webmaster 站点验证相关
location ~* (robots\.txt|favicon\.ico|crossdomain\.xml|google4c90d18e696bdcf8\.html|BingSiteAuth\.xml)$ {
root /data/eiblog/static;
expires 1d;
}
# imququ 的上传文件相关,未用到
location ^~ /static/uploads/ {
root /home/jerry/www/imququ.com/www;
add_header Access-Control-Allow-Origin *;
set $expires_time max;
valid_referers blocked none server_names *.qgy18.com *.inoreader.com feedly.com *.feedly.com www.udpwork.com theoldreader.com digg.com *.feiworks.com *.newszeit.com r.mail.qq.com yuedu.163.com *.w3ctech.com;
if ($invalid_referer) {
set $expires_time -1;
return 403;
}
expires $expires_time;
}
location ^~ /static/ {
root /data/eiblog;
add_header Access-Control-Allow-Origin *;
expires max;
}
location ^~ /admin/ {
proxy_http_version 1.1;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
# deny 将完全不允许页面被嵌套,可能会导致一些异常。如果遇到这样的问题,建议改成 SAMEORIGIN
# https://imququ.com/post/web-security-and-response-header.html#toc-1
add_header X-Frame-Options deny;
add_header X-Content-Type-Options nosniff;
proxy_set_header Connection "";
proxy_set_header Host deepzz.com;
proxy_set_header X-Real_IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9000;
}
location / {
proxy_http_version 1.1;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header X-Frame-Options deny;
add_header X-Content-Type-Options nosniff;
# 改deepzz相关的
add_header Content-Security-Policy "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' blob: https:; img-src data: https: https://st.deepzz.com; style-src 'unsafe-inline' https:; child-src https:; connect-src 'self' https://translate.googleapis.com; frame-src https://disqus.com https://www.slideshare.net";
# 中间证书证书指纹
# https://imququ.com/post/http-public-key-pinning.html
add_header Public-Key-Pins 'pin-sha256="lnsM2T/O9/J84sJFdnrpsFp3awZJ+ZZbYpCWhGloaHI="; pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; max-age=2592000; includeSubDomains';
add_header Cache-Control no-cache;
add_header X-Via Aliyun.QingDao;
add_header X-XSS-Protection "1; mode=block";
proxy_ignore_headers Set-Cookie;
proxy_hide_header Vary;
proxy_hide_header X-Powered-By;
proxy_set_header Connection "";
proxy_set_header Host deepzz.com;
proxy_set_header X-Real_IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9000;
}
}
server {
server_name www.deepzz.com deepzz.com;
server_tokens off;
access_log /dev/null;
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
location ^~ /.well-known/acme-challenge/ {
alias /data/letsencrypt/challenges/;
try_files $uri =404;
}
location / {
rewrite ^/(.*)$ https://deepzz.com/$1 permanent;
}
}

View File

@@ -0,0 +1,138 @@
server {
listen 443 ssl http2 fastopen=3 reuseport;
server_name www.deepzz.com deepzz.com;
server_tokens off;
access_log /data/eiblog/logdata/nginx.log;
# IP黑名单.
include /data/eiblog/conf/nginx/ip.blacklist;
# letsencrypt v2已内置, 忽略.
# https://imququ.com/post/certificate-transparency.html#toc-2
#ssl_ct on;
#ssl_ct_static_scts /data/eiblog/conf/scts/rsa/;
#ssl_ct_static_scts /data/eiblog/conf/scts/ecc/;
# 中间证书 + 根证书.
# https://imququ.com/post/why-can-not-turn-on-ocsp-stapling.html
ssl_trusted_certificate /data/eiblog/conf/ssl/full_chained.pem;
# 站点证书 + 中间证书, 私钥.
ssl_certificate /data/eiblog/conf/ssl/domain.rsa.pem;
ssl_certificate_key /data/eiblog/conf/ssl/domain.rsa.key;
# ssl_certificate /data/eiblog/conf/ssl/domain.ecc.pem;
# ssl_certificate_key /data/eiblog/conf/ssl/domain.ecc.key;
# openssl dhparam -out dhparams.pem 2048
# https://weakdh.org/sysadmin.html
ssl_dhparam /data/eiblog/conf/ssl/dhparams.pem;
# 单机部署可以不指定.
# openssl rand 48 > session_ticket.key
# ssl_session_ticket_key /data/eiblog/conf/ssl/session_ticket.key;
ssl_prefer_server_ciphers on;
# https://github.com/cloudflare/sslconfig/blob/master/conf
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
# 如果启用 RSA + ECDSA 双证书, Cipher Suite 可以参考以下配置.
# ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets on;
# ssl stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 114.114.114.114 8.8.8.8 valid=300s;
resolver_timeout 10s;
if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) {
return 444;
}
if ($host != 'deepzz.com' ) {
rewrite ^/(.*)$ https://deepzz.com/$1 permanent;
}
# webmaster 站点验证相关.
location ~* (google4c90d18e696bdcf8\.html|BingSiteAuth\.xml)$ {
root /data/eiblog/static;
expires 1d;
}
location ^~ /admin/ {
proxy_http_version 1.1;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
# deny 将完全不允许页面被嵌套,可能会导致一些异常。如果遇到这样的问题,建议改成 SAMEORIGIN.
# https://imququ.com/post/web-security-and-response-header.html#toc-1
add_header X-Frame-Options deny;
add_header X-Powered-By eiblog/1.3.0;
add_header X-Content-Type-Options nosniff;
proxy_set_header Connection "";
proxy_set_header Host deepzz.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9000;
}
location / {
proxy_http_version 1.1;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header X-Frame-Options deny;
add_header X-Content-Type-Options nosniff;
# 改deepzz相关的.
add_header Content-Security-Policy "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' blob: https:; img-src data: https: https://st.deepzz.com; media-src https://st.deepzz.com; style-src 'unsafe-inline' https:; child-src https:; connect-src 'self' https://translate.googleapis.com; frame-src https://disqus.com https://www.slideshare.net";
# 中间证书证书指纹, chrome69 将忽略, 用 expect-ct 替代.
# https://imququ.com/post/http-public-key-pinning.html
#add_header Public-Key-Pins 'pin-sha256="IiSbZ4pMDEyXvtl7Lg8K3FNmJcTAhKUTrB2FQOaAO/s="; pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; max-age=2592000;';
# 期望 ct.
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
add_header Expect-CT "max-age=180";
add_header Cache-Control no-cache;
add_header X-Via Aliyun.QingDao;
add_header X-XSS-Protection "1; mode=block";
add_header X-Powered-By eiblog/1.3.0;
proxy_ignore_headers Set-Cookie;
proxy_hide_header Vary;
proxy_set_header Connection "";
proxy_set_header Host deepzz.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9000;
}
}
server {
server_name www.deepzz.com deepzz.com;
server_tokens off;
access_log /dev/null;
if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) {
return 444;
}
# letsencrypt file verify
location ^~ /.well-known/acme-challenge/ {
alias /data/eiblog/challenges/;
try_files $uri =404;
}
location / {
rewrite ^/(.*)$ https://deepzz.com/$1 permanent;
}
}

View File

@@ -1,24 +0,0 @@
# 博主原博客
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,2 @@
# example black list
#deny 195.154.211.220;

Binary file not shown.

Binary file not shown.

425
db.go
View File

@@ -13,9 +13,7 @@ import (
"github.com/eiblog/blackfriday"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
db "github.com/eiblog/utils/mgo"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"github.com/eiblog/utils/mgo"
)
// 数据库及表名
@@ -62,44 +60,24 @@ var (
func init() {
// 数据库加索引
ms, c := db.Connect(DB, COLLECTION_ACCOUNT)
index := mgo.Index{
Key: []string{"username"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err := c.EnsureIndex(index); err != nil {
err := mgo.Index(DB, COLLECTION_ACCOUNT, []string{"username"})
if err != nil {
logd.Fatal(err)
}
ms.Close()
ms, c = db.Connect(DB, COLLECTION_ARTICLE)
index = mgo.Index{
Key: []string{"id"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err := c.EnsureIndex(index); err != nil {
err = mgo.Index(DB, COLLECTION_ARTICLE, []string{"id"})
if err != nil {
logd.Fatal(err)
}
index = mgo.Index{
Key: []string{"slug"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err := c.EnsureIndex(index); err != nil {
err = mgo.Index(DB, COLLECTION_ARTICLE, []string{"slug"})
if err != nil {
logd.Fatal(err)
}
ms.Close()
// 读取帐号信息
Ei = loadAccount()
loadAccount()
// 获取文章数据
Ei.Articles = loadArticles()
loadArticles()
// 生成markdown文档
go generateMarkdown()
// 启动定时器
@@ -109,12 +87,13 @@ func init() {
}
// 读取或初始化帐号信息
func loadAccount() (a *Account) {
a = &Account{}
err := db.FindOne(DB, COLLECTION_ACCOUNT, bson.M{"username": setting.Conf.Account.Username}, a)
func loadAccount() {
Ei = &Account{}
err := mgo.FindOne(DB, COLLECTION_ACCOUNT, mgo.M{"username": setting.Conf.Account.Username}, Ei)
// 初始化用户数据
if err == mgo.ErrNotFound {
a = &Account{
logd.Printf("Initializing account: %s\n", setting.Conf.Account.Username)
Ei = &Account{
Username: setting.Conf.Account.Username,
Password: EncryptPasswd(setting.Conf.Account.Username, setting.Conf.Account.Password),
Email: setting.Conf.Account.Email,
@@ -122,49 +101,45 @@ func loadAccount() (a *Account) {
Address: setting.Conf.Account.Address,
CreateTime: time.Now(),
}
a.BlogName = setting.Conf.Blogger.BlogName
a.SubTitle = setting.Conf.Blogger.SubTitle
a.BeiAn = setting.Conf.Blogger.BeiAn
a.BTitle = setting.Conf.Blogger.BTitle
a.Copyright = setting.Conf.Blogger.Copyright
err = db.Insert(DB, COLLECTION_ACCOUNT, a)
Ei.BlogName = setting.Conf.Blogger.BlogName
Ei.SubTitle = setting.Conf.Blogger.SubTitle
Ei.BeiAn = setting.Conf.Blogger.BeiAn
Ei.BTitle = setting.Conf.Blogger.BTitle
Ei.Copyright = setting.Conf.Blogger.Copyright
err = mgo.Insert(DB, COLLECTION_ACCOUNT, Ei)
generateTopic()
} else if err != nil {
logd.Fatal(err)
}
a.CH = make(chan string, 2)
a.MapArticles = make(map[string]*Article)
a.Tags = make(map[string]SortArticles)
return
Ei.CH = make(chan string, 2)
Ei.MapArticles = make(map[string]*Article)
Ei.Tags = make(map[string]SortArticles)
}
func loadArticles() (artcs SortArticles) {
err := db.FindAll(DB, COLLECTION_ARTICLE, bson.M{"isdraft": false, "deletetime": bson.M{"$eq": time.Time{}}}, &artcs)
func loadArticles() {
err := mgo.FindAll(DB, COLLECTION_ARTICLE, mgo.M{"isdraft": false, "deletetime": mgo.M{"$eq": time.Time{}}}, &Ei.Articles)
if err != nil {
logd.Fatal(err)
}
sort.Sort(artcs)
for i, v := range artcs {
sort.Sort(Ei.Articles)
for i, v := range Ei.Articles {
// 渲染文章
GenerateExcerptAndRender(v)
Ei.MapArticles[v.Slug] = v
// 分析文章
if v.ID < setting.Conf.StartID {
if v.ID < setting.Conf.General.StartID {
continue
}
if i > 0 {
v.Prev = artcs[i-1]
v.Prev = Ei.Articles[i-1]
}
if artcs[i+1].ID >= setting.Conf.StartID {
v.Next = artcs[i+1]
if Ei.Articles[i+1].ID >= setting.Conf.General.StartID {
v.Next = Ei.Articles[i+1]
}
ManageTagsArticle(v, false, ADD)
ManageSeriesArticle(v, false, ADD)
ManageArchivesArticle(v, false, ADD)
upArticle(v, false)
}
Ei.CH <- SERIES_MD
Ei.CH <- ARCHIVE_MD
return
}
// generate series,archive markdown
@@ -183,7 +158,8 @@ func generateMarkdown() {
buffer.WriteString("\n\n")
for _, artc := range serie.Articles {
//eg. * [标题一](/post/hello-world.html) <span class="date">(Man 02, 2006)</span>
buffer.WriteString("* [" + artc.Title + "](/post/" + artc.Slug + ".html) <span class=\"date\">(" + artc.CreateTime.Format("Jan 02, 2006") + ")</span>\n")
buffer.WriteString("* [" + artc.Title + "](/post/" + artc.Slug +
".html) <span class=\"date\">(" + artc.CreateTime.Format("Jan 02, 2006") + ")</span>\n")
}
buffer.WriteByte('\n')
}
@@ -191,15 +167,31 @@ func generateMarkdown() {
case ARCHIVE_MD:
sort.Sort(Ei.Archives)
var buffer bytes.Buffer
buffer.WriteString(Ei.ArchivesSay)
buffer.WriteString("\n\n")
buffer.WriteString(Ei.ArchivesSay + "\n")
var (
currentYear string
gt12Month = len(Ei.Archives) > 12
)
for _, archive := range Ei.Archives {
buffer.WriteString(fmt.Sprintf("### %s", archive.Time.Format("2006年01月")))
buffer.WriteString("\n\n")
for _, artc := range archive.Articles {
buffer.WriteString("* [" + artc.Title + "](/post/" + artc.Slug + ".html) <span class=\"date\">(" + artc.CreateTime.Format("Jan 02, 2006") + ")</span>\n")
if gt12Month {
year := archive.Time.Format("2006 年")
if currentYear != year {
currentYear = year
buffer.WriteString(fmt.Sprintf("\n### %s\n\n", archive.Time.Format("2006 年")))
}
} else {
buffer.WriteString(fmt.Sprintf("\n### %s\n\n", archive.Time.Format("2006年1月")))
}
for i, artc := range archive.Articles {
if i == 0 && gt12Month {
buffer.WriteString("* *[" + artc.Title + "](/post/" + artc.Slug +
".html) <span class=\"date\">(" + artc.CreateTime.Format("Jan 02, 2006") + ")</span>*\n")
} else {
buffer.WriteString("* [" + artc.Title + "](/post/" + artc.Slug +
".html) <span class=\"date\">(" + artc.CreateTime.Format("Jan 02, 2006") + ")</span>\n")
}
}
buffer.WriteByte('\n')
}
Ei.PageArchives = string(renderPage(buffer.Bytes()))
}
@@ -209,26 +201,29 @@ func generateMarkdown() {
// init account: generate blogroll and about page
func generateTopic() {
about := &Article{
ID: db.NextVal(DB, COUNTER_ARTICLE),
ID: mgo.NextVal(DB, COUNTER_ARTICLE),
Author: setting.Conf.Account.Username,
Title: "关于",
Slug: "about",
CreateTime: time.Now(),
CreateTime: time.Time{},
UpdateTime: time.Time{},
}
// 推送到 disqus
go func() { ThreadCreate(about) }()
blogroll := &Article{
ID: db.NextVal(DB, COUNTER_ARTICLE),
ID: mgo.NextVal(DB, COUNTER_ARTICLE),
Author: setting.Conf.Account.Username,
Title: "友情链接",
Slug: "blogroll",
UpdateTime: time.Now(),
CreateTime: time.Time{},
UpdateTime: time.Time{},
}
err := db.Insert(DB, COLLECTION_ARTICLE, blogroll)
err := mgo.Insert(DB, COLLECTION_ARTICLE, blogroll)
if err != nil {
logd.Fatal(err)
}
err = db.Insert(DB, COLLECTION_ARTICLE, about)
err = mgo.Insert(DB, COLLECTION_ARTICLE, about)
if err != nil {
logd.Fatal(err)
}
@@ -244,7 +239,7 @@ func renderPage(md []byte) []byte {
func PageList(p, n int) (prev int, next int, artcs []*Article) {
var l int
for l = len(Ei.Articles); l > 0; l-- {
if Ei.Articles[l-1].ID >= setting.Conf.StartID {
if Ei.Articles[l-1].ID >= setting.Conf.General.StartID {
break
}
}
@@ -273,107 +268,20 @@ func PageList(p, n int) (prev int, next int, artcs []*Article) {
return
}
func ManageTagsArticle(artc *Article, s bool, do string) {
switch do {
case ADD:
for _, tag := range artc.Tags {
Ei.Tags[tag] = append(Ei.Tags[tag], artc)
if s {
sort.Sort(Ei.Tags[tag])
}
}
case DELETE:
for _, tag := range artc.Tags {
for i, v := range Ei.Tags[tag] {
if v == artc {
Ei.Tags[tag] = append(Ei.Tags[tag][0:i], Ei.Tags[tag][i+1:]...)
if len(Ei.Tags[tag]) == 0 {
delete(Ei.Tags, tag)
}
return
}
}
}
}
}
func ManageSeriesArticle(artc *Article, s bool, do string) {
switch do {
case ADD:
for i, serie := range Ei.Series {
if serie.ID == artc.SerieID {
Ei.Series[i].Articles = append(Ei.Series[i].Articles, artc)
if s {
sort.Sort(Ei.Series[i].Articles)
Ei.CH <- SERIES_MD
return
}
}
}
case DELETE:
for i, serie := range Ei.Series {
if serie.ID == artc.SerieID {
for j, v := range serie.Articles {
if v == artc {
Ei.Series[i].Articles = append(Ei.Series[i].Articles[0:j], Ei.Series[i].Articles[j+1:]...)
Ei.CH <- SERIES_MD
return
}
}
}
}
}
}
func ManageArchivesArticle(artc *Article, s bool, do string) {
switch do {
case ADD:
add := false
y, m, _ := artc.CreateTime.Date()
for i, archive := range Ei.Archives {
ay, am, _ := archive.Time.Date()
if y == ay && m == am {
add = true
Ei.Archives[i].Articles = append(Ei.Archives[i].Articles, artc)
if s {
sort.Sort(Ei.Archives[i].Articles)
Ei.CH <- ARCHIVE_MD
break
}
}
}
if !add {
Ei.Archives = append(Ei.Archives, &Archive{Time: artc.CreateTime, Articles: SortArticles{artc}})
}
case DELETE:
for i, archive := range Ei.Archives {
ay, am, _ := archive.Time.Date()
if y, m, _ := artc.CreateTime.Date(); ay == y && am == m {
for j, v := range archive.Articles {
if v == artc {
Ei.Archives[i].Articles = append(Ei.Archives[i].Articles[0:j], Ei.Archives[i].Articles[j+1:]...)
Ei.CH <- ARCHIVE_MD
return
}
}
}
}
}
}
// 渲染markdown操作和截取摘要操作
var reg = regexp.MustCompile(setting.Conf.Identifier)
var reg = regexp.MustCompile(setting.Conf.General.Identifier)
// header
var regH = regexp.MustCompile("</nav></div>")
func GenerateExcerptAndRender(artc *Article) {
if strings.HasPrefix(artc.Content, setting.Conf.Description) {
if strings.HasPrefix(artc.Content, setting.Conf.General.DescPrefix) {
index := strings.Index(artc.Content, "\r\n")
artc.Desc = IgnoreHtmlTag(artc.Content[len(setting.Conf.Description):index])
artc.Desc = IgnoreHtmlTag(artc.Content[len(setting.Conf.General.DescPrefix):index])
artc.Content = artc.Content[index:]
}
// 查找目录
content := renderPage([]byte(artc.Content))
index := regH.FindIndex(content)
if index != nil {
@@ -387,7 +295,7 @@ func GenerateExcerptAndRender(artc *Article) {
artc.Excerpt = IgnoreHtmlTag(artc.Content[0:index[0]])
} else {
uc := []rune(artc.Content)
length := setting.Conf.Length
length := setting.Conf.General.Length
if len(uc) < length {
length = len(uc)
}
@@ -397,44 +305,152 @@ func GenerateExcerptAndRender(artc *Article) {
// 读取草稿箱
func LoadDraft() (artcs SortArticles, err error) {
err = db.FindAll(DB, COLLECTION_ARTICLE, bson.M{"isdraft": true}, &artcs)
err = mgo.FindAll(DB, COLLECTION_ARTICLE, mgo.M{"isdraft": true}, &artcs)
sort.Sort(artcs)
return
}
// 读取回收箱
func LoadTrash() (artcs SortArticles, err error) {
err = db.FindAll(DB, COLLECTION_ARTICLE, bson.M{"deletetime": bson.M{"$ne": time.Time{}}}, &artcs)
err = mgo.FindAll(DB, COLLECTION_ARTICLE, mgo.M{"deletetime": mgo.M{"$ne": time.Time{}}}, &artcs)
sort.Sort(artcs)
return
}
// 添加文章到tag、serie、archive
func upArticle(artc *Article, needSort bool) {
// tag
for _, tag := range artc.Tags {
Ei.Tags[tag] = append(Ei.Tags[tag], artc)
if needSort {
sort.Sort(Ei.Tags[tag])
}
}
// serie
for i, serie := range Ei.Series {
if serie.ID == artc.SerieID {
Ei.Series[i].Articles = append(Ei.Series[i].Articles, artc)
if needSort {
sort.Sort(Ei.Series[i].Articles)
Ei.CH <- SERIES_MD
}
break
}
}
// archive
y, m, _ := artc.CreateTime.Date()
for i, archive := range Ei.Archives {
if ay, am, _ := archive.Time.Date(); y == ay && m == am {
Ei.Archives[i].Articles = append(Ei.Archives[i].Articles, artc)
if needSort {
sort.Sort(Ei.Archives[i].Articles)
Ei.CH <- ARCHIVE_MD
}
return
}
}
Ei.Archives = append(Ei.Archives, &Archive{Time: artc.CreateTime,
Articles: SortArticles{artc}})
if needSort {
Ei.CH <- ARCHIVE_MD
}
}
// 删除文章从tag、serie、archive
func dropArticle(artc *Article) {
// tag
for _, tag := range artc.Tags {
for i, v := range Ei.Tags[tag] {
if v == artc {
Ei.Tags[tag] = append(Ei.Tags[tag][0:i], Ei.Tags[tag][i+1:]...)
if len(Ei.Tags[tag]) == 0 {
delete(Ei.Tags, tag)
}
}
}
}
// serie
for i, serie := range Ei.Series {
if serie.ID == artc.SerieID {
for j, v := range serie.Articles {
if v == artc {
Ei.Series[i].Articles = append(Ei.Series[i].Articles[0:j],
Ei.Series[i].Articles[j+1:]...)
Ei.CH <- SERIES_MD
break
}
}
}
}
// archive
for i, archive := range Ei.Archives {
ay, am, _ := archive.Time.Date()
if y, m, _ := artc.CreateTime.Date(); ay == y && am == m {
for j, v := range archive.Articles {
if v == artc {
Ei.Archives[i].Articles = append(Ei.Archives[i].Articles[0:j],
Ei.Archives[i].Articles[j+1:]...)
if len(Ei.Archives[i].Articles) == 0 {
Ei.Archives = append(Ei.Archives[:i], Ei.Archives[i+1:]...)
}
Ei.CH <- ARCHIVE_MD
break
}
}
}
}
}
// 替换文章
func ReplaceArticle(oldArtc *Article, newArtc *Article) {
Ei.MapArticles[newArtc.Slug] = newArtc
GenerateExcerptAndRender(newArtc)
if newArtc.ID < setting.Conf.General.StartID {
return
}
if oldArtc != nil {
i, artc := GetArticle(oldArtc.ID)
DelFromLinkedList(artc)
Ei.Articles = append(Ei.Articles[:i], Ei.Articles[i+1:]...)
dropArticle(oldArtc)
}
Ei.Articles = append(Ei.Articles, newArtc)
sort.Sort(Ei.Articles)
AddToLinkedList(newArtc.ID)
upArticle(newArtc, true)
}
// 添加文章
func AddArticle(artc *Article) error {
// 分配ID, 占位至起始id
for {
if id := db.NextVal(DB, COUNTER_ARTICLE); id < setting.Conf.StartID {
if id := mgo.NextVal(DB, COUNTER_ARTICLE); id < setting.Conf.General.StartID {
continue
} else {
artc.ID = id
break
}
}
err := mgo.Insert(DB, COLLECTION_ARTICLE, artc)
if err != nil {
return err
}
// 正式发布文章
if !artc.IsDraft {
defer GenerateExcerptAndRender(artc)
Ei.MapArticles[artc.Slug] = artc
Ei.Articles = append([]*Article{artc}, Ei.Articles...)
sort.Sort(Ei.Articles)
AddToLinkedList(artc.ID)
ManageTagsArticle(artc, true, ADD)
ManageSeriesArticle(artc, true, ADD)
ManageArchivesArticle(artc, true, ADD)
Ei.CH <- ARCHIVE_MD
if artc.SerieID > 0 {
Ei.CH <- SERIES_MD
}
upArticle(artc, true)
}
return db.Insert(DB, COLLECTION_ARTICLE, artc)
return nil
}
// 删除文章,移入回收箱
@@ -446,20 +462,17 @@ func DelArticles(ids ...int32) error {
DelFromLinkedList(artc)
Ei.Articles = append(Ei.Articles[:i], Ei.Articles[i+1:]...)
delete(Ei.MapArticles, artc.Slug)
ManageTagsArticle(artc, false, DELETE)
ManageSeriesArticle(artc, false, DELETE)
ManageArchivesArticle(artc, false, DELETE)
err := UpdateArticle(bson.M{"id": id}, bson.M{"$set": bson.M{"deletetime": time.Now()}})
err := UpdateArticle(mgo.M{"id": id}, mgo.M{"$set": mgo.M{"deletetime": time.Now()}})
if err != nil {
return err
}
artc = nil
dropArticle(artc)
}
Ei.CH <- ARCHIVE_MD
Ei.CH <- SERIES_MD
return nil
}
// 从链表里删除文章
func DelFromLinkedList(artc *Article) {
if artc.Prev == nil && artc.Next != nil {
artc.Next.Prev = nil
@@ -471,12 +484,13 @@ func DelFromLinkedList(artc *Article) {
}
}
// 将文章添加到链表
func AddToLinkedList(id int32) {
i, artc := GetArticle(id)
if i == 0 && Ei.Articles[i+1].ID >= setting.Conf.StartID {
if i == 0 && Ei.Articles[i+1].ID >= setting.Conf.General.StartID {
artc.Next = Ei.Articles[i+1]
Ei.Articles[i+1].Prev = artc
} else if i > 0 && Ei.Articles[i-1].ID >= setting.Conf.StartID {
} else if i > 0 && Ei.Articles[i-1].ID >= setting.Conf.General.StartID {
artc.Prev = Ei.Articles[i-1]
if Ei.Articles[i-1].Next != nil {
artc.Next = Ei.Articles[i-1].Next
@@ -498,37 +512,39 @@ func GetArticle(id int32) (int, *Article) {
// 定时清除回收箱文章
func timer() {
delT := time.NewTicker(time.Duration(setting.Conf.Clean) * time.Hour)
delT := time.NewTicker(time.Duration(setting.Conf.General.Clean) * time.Hour)
for {
<-delT.C
db.Remove(DB, COLLECTION_ARTICLE, bson.M{"deletetime": bson.M{"$gt": time.Time{}, "$lt": time.Now().Add(time.Duration(setting.Conf.Trash) * time.Hour)}})
mgo.Remove(DB, COLLECTION_ARTICLE, mgo.M{"deletetime": mgo.M{"$gt": time.Time{},
"$lt": time.Now().Add(time.Duration(setting.Conf.General.Trash) * time.Hour)}})
}
}
// 操作帐号字段
func UpdateAccountField(M bson.M) error {
return db.Update(DB, COLLECTION_ACCOUNT, bson.M{"username": Ei.Username}, M)
func UpdateAccountField(M mgo.M) error {
return mgo.Update(DB, COLLECTION_ACCOUNT, mgo.M{"username": Ei.Username}, M)
}
// 删除草稿箱或回收箱,永久删除
func RemoveArticle(id int32) error {
return db.Remove(DB, COLLECTION_ARTICLE, bson.M{"id": id})
return mgo.Remove(DB, COLLECTION_ARTICLE, mgo.M{"id": id})
}
// 恢复删除文章到草稿箱
func RecoverArticle(id int32) error {
return db.Update(DB, COLLECTION_ARTICLE, bson.M{"id": id}, bson.M{"$set": bson.M{"deletetime": time.Time{}, "isdraft": true}})
return mgo.Update(DB, COLLECTION_ARTICLE, mgo.M{"id": id},
mgo.M{"$set": mgo.M{"deletetime": time.Time{}, "isdraft": true}})
}
// 更新文章
func UpdateArticle(query, update interface{}) error {
return db.Update(DB, COLLECTION_ARTICLE, query, update)
return mgo.Update(DB, COLLECTION_ARTICLE, query, update)
}
// 编辑文档
func QueryArticle(id int32) *Article {
artc := &Article{}
if err := db.FindOne(DB, COLLECTION_ARTICLE, bson.M{"id": id}, artc); err != nil {
if err := mgo.FindOne(DB, COLLECTION_ARTICLE, mgo.M{"id": id}, artc); err != nil {
return nil
}
return artc
@@ -536,17 +552,18 @@ func QueryArticle(id int32) *Article {
// 添加专题
func AddSerie(name, slug, desc string) error {
serie := &Serie{db.NextVal(DB, COUNTER_SERIE), name, slug, desc, time.Now(), nil}
serie := &Serie{mgo.NextVal(DB, COUNTER_SERIE), name, slug, desc, time.Now(), nil}
Ei.Series = append(Ei.Series, serie)
sort.Sort(Ei.Series)
Ei.CH <- SERIES_MD
return UpdateAccountField(bson.M{"$addToSet": bson.M{"blogger.series": serie}})
return UpdateAccountField(mgo.M{"$addToSet": mgo.M{"blogger.series": serie}})
}
// 更新专题
func UpdateSerie(serie *Serie) error {
Ei.CH <- SERIES_MD
return db.Update(DB, COLLECTION_ACCOUNT, bson.M{"username": Ei.Username, "blogger.series.id": serie.ID}, bson.M{"$set": bson.M{"blogger.series.$": serie}})
return mgo.Update(DB, COLLECTION_ACCOUNT, mgo.M{"username": Ei.Username,
"blogger.series.id": serie.ID}, mgo.M{"$set": mgo.M{"blogger.series.$": serie}})
}
// 删除专题
@@ -556,7 +573,7 @@ func DelSerie(id int32) error {
if len(serie.Articles) > 0 {
return fmt.Errorf("请删除该专题下的所有文章")
}
err := UpdateAccountField(bson.M{"$pull": bson.M{"blogger.series": bson.M{"id": id}}})
err := UpdateAccountField(mgo.M{"$pull": mgo.M{"blogger.series": mgo.M{"id": id}}})
if err != nil {
return err
}
@@ -580,24 +597,24 @@ func QuerySerie(id int32) *Serie {
// 后台分页
func PageListBack(se int, kw string, draft, del bool, p, n int) (max int, artcs []*Article) {
M := bson.M{}
M := mgo.M{}
if draft {
M["isdraft"] = true
} else if del {
M["deletetime"] = bson.M{"$ne": time.Time{}}
M["deletetime"] = mgo.M{"$ne": time.Time{}}
} else {
M["isdraft"] = false
M["deletetime"] = bson.M{"$eq": time.Time{}}
M["deletetime"] = mgo.M{"$eq": time.Time{}}
if se > 0 {
M["serieid"] = se
}
if kw != "" {
M["title"] = bson.M{"$regex": kw, "$options": "$i"}
M["title"] = mgo.M{"$regex": kw, "$options": "$i"}
}
}
ms, c := db.Connect(DB, COLLECTION_ARTICLE)
ms, c := mgo.Connect(DB, COLLECTION_ARTICLE)
defer ms.Close()
err := c.Find(M).Select(bson.M{"content": 0}).Sort("-createtime").Limit(n).Skip((p - 1) * n).All(&artcs)
err := c.Find(M).Select(mgo.M{"content": 0}).Sort("-createtime").Limit(n).Skip((p - 1) * n).All(&artcs)
if err != nil {
logd.Error(err)
}

389
disqus.go
View File

@@ -3,183 +3,378 @@
package main
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/deepzz0/logd"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
)
type result struct {
var ErrDisqusConfig = errors.New("disqus config incorrect")
func correctDisqusConfig() bool {
return setting.Conf.Disqus.PostsCount != "" &&
setting.Conf.Disqus.PublicKey != "" &&
setting.Conf.Disqus.ShortName != ""
}
// 定时获取所有文章评论数量
type postsCountResp struct {
Code int
Response []struct {
Id string
Posts int
Identifiers []string
}
}
func PostsCount() {
if setting.Conf.Disqus.PostsCount == "" || setting.Conf.Disqus.PublicKey == "" || setting.Conf.Disqus.ShortName == "" {
return
func PostsCount() error {
if !correctDisqusConfig() {
return ErrDisqusConfig
}
baseUrl := setting.Conf.Disqus.PostsCount +
"?api_key=" + setting.Conf.Disqus.PublicKey +
"&forum=" + setting.Conf.Disqus.ShortName + "&"
time.AfterFunc(time.Duration(setting.Conf.Disqus.Interval)*time.Hour, func() {
err := PostsCount()
if err != nil {
logd.Error(err)
}
})
vals := url.Values{}
vals.Set("api_key", setting.Conf.Disqus.PublicKey)
vals.Set("forum", setting.Conf.Disqus.ShortName)
var count, index int
for index < len(Ei.Articles) {
logd.Debugf("count=====%d, index=======%d, length=======%d, bool=========%t\n", count, index, len(Ei.Articles), index < len(Ei.Articles) && count < 50)
var threads []string
for ; index < len(Ei.Articles) && count < 50; index++ {
artc := Ei.Articles[index]
threads = append(threads, fmt.Sprintf("thread:ident=post-%s", artc.Slug))
vals.Add("thread:ident", "post-"+artc.Slug)
count++
}
count = 0
url := baseUrl + strings.Join(threads, "&")
resp, err := http.Get(url)
resp, err := Get(setting.Conf.Disqus.PostsCount + "?" + vals.Encode())
if err != nil {
logd.Error(err)
break
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
logd.Error(err)
break
return err
}
if resp.StatusCode != http.StatusOK {
logd.Error(string(b))
break
return errors.New(string(b))
}
rst := result{}
err = json.Unmarshal(b, &rst)
result := &postsCountResp{}
err = json.Unmarshal(b, result)
if err != nil {
logd.Error(err)
break
return err
}
for _, v := range rst.Response {
for _, v := range result.Response {
i := strings.Index(v.Identifiers[0], "-")
artc := Ei.MapArticles[v.Identifiers[0][i+1:]]
if artc != nil {
artc.Count = v.Posts
artc.Thread = v.Id
}
}
}
time.AfterFunc(time.Duration(setting.Conf.Disqus.Interval)*time.Hour, PostsCount)
return nil
}
type postsList struct {
// 获取文章评论列表
type postsListResp struct {
Cursor struct {
HasNext bool
Next string
}
Code int
Response []struct {
Parent int
Id string
CreatedAt string
Message string
Author struct {
Name string
ProfileUrl string
Avatar struct {
Cache string
}
Response []postDetail
}
type postDetail struct {
Parent int
Id string
CreatedAt string
Message string
IsDeleted bool
Author struct {
Name string
ProfileUrl string
Avatar struct {
Cache string
}
Thread string
}
Thread string
}
func PostsList(slug, cursor string) *postsList {
if setting.Conf.Disqus.PostsList == "" || setting.Conf.Disqus.PublicKey == "" || setting.Conf.Disqus.ShortName == "" {
return nil
func PostsList(slug, cursor string) (*postsListResp, error) {
if !correctDisqusConfig() {
return nil, ErrDisqusConfig
}
url := setting.Conf.Disqus.PostsList + "?limit=50&api_key=" +
setting.Conf.Disqus.PublicKey + "&forum=" + setting.Conf.Disqus.ShortName +
"&cursor=" + cursor + "&thread:ident=post-" + slug
resp, err := http.Get(url)
vals := url.Values{}
vals.Set("api_key", setting.Conf.Disqus.PublicKey)
vals.Set("forum", setting.Conf.Disqus.ShortName)
vals.Set("thread:ident", "post-"+slug)
vals.Set("cursor", cursor)
vals.Set("limit", "50")
resp, err := Get(setting.Conf.Disqus.PostsList + "?" + vals.Encode())
if err != nil {
logd.Error(err)
return nil
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(b))
}
result := &postsListResp{}
err = json.Unmarshal(b, result)
if err != nil {
return nil, err
}
return result, nil
}
type PostComment struct {
Message string
Parent string
Thread string
AuthorEmail string
AuthorName string
IpAddress string
Identifier string
UserAgent string
}
type postCreateResp struct {
Code int
Response postDetail
}
// 评论文章
func PostCreate(pc *PostComment) (*postCreateResp, error) {
if !correctDisqusConfig() {
return nil, ErrDisqusConfig
}
vals := url.Values{}
vals.Set("api_key", "E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F")
vals.Set("message", pc.Message)
vals.Set("parent", pc.Parent)
vals.Set("thread", pc.Thread)
vals.Set("author_email", pc.AuthorEmail)
vals.Set("author_name", pc.AuthorName)
// vals.Set("state", "approved")
header := http.Header{"Referer": {"https://disqus.com"}}
resp, err := PostWithHeader(setting.Conf.Disqus.PostCreate, vals, header)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
logd.Error(err)
return nil
return nil, err
}
if resp.StatusCode != http.StatusOK {
logd.Error(string(b))
return nil
return nil, errors.New(string(b))
}
pl := &postsList{}
err = json.Unmarshal(b, pl)
result := &postCreateResp{}
err = json.Unmarshal(b, result)
if err != nil {
logd.Error(err)
return nil
return nil, err
}
return pl
return result, nil
}
type PostCreate struct {
Message string `json:"message"`
Parent string `json:"parent"`
Thread string `json:"thread"`
AuthorEmail string `json:"author_email"`
AuthorName string `json:"autor_name"`
IpAddress string `json:"ip_address"`
Identifier string `json:"identifier"`
UserAgent string `json:"user_agent"`
// 批准评论通过
type approvedResp struct {
Code int
Response []struct {
Id string
}
}
type PostResponse struct {
Code int `json:"code"`
func PostApprove(post string) error {
if !correctDisqusConfig() {
return ErrDisqusConfig
}
vals := url.Values{}
vals.Set("api_key", setting.Conf.Disqus.PublicKey)
vals.Set("access_token", setting.Conf.Disqus.AccessToken)
vals.Set("post", post)
header := http.Header{"Referer": {"https://disqus.com"}}
resp, err := PostWithHeader(setting.Conf.Disqus.PostApprove, vals, header)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New(string(b))
}
result := &approvedResp{}
err = json.Unmarshal(b, result)
if err != nil {
return err
}
return nil
}
// 创建thread
type threadCreateResp struct {
Code int
Response struct {
Id string `json:"id"`
} `json:"response"`
Id string
}
}
func PostComment(pc *PostCreate) string {
if setting.Conf.Disqus.PostsList == "" || setting.Conf.Disqus.PublicKey == "" || setting.Conf.Disqus.ShortName == "" {
return ""
func ThreadCreate(artc *Article) error {
if !correctDisqusConfig() {
return ErrDisqusConfig
}
url := setting.Conf.Disqus.PostCreate +
"?api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F" +
"&message=" + pc.Message + "&parent=" + pc.Parent +
"&thread=" + pc.Thread + "&author_email=" + pc.AuthorEmail +
"&author_name=" + pc.AuthorName
request, err := http.NewRequest("POST", url, nil)
vals := url.Values{}
vals.Set("api_key", setting.Conf.Disqus.PublicKey)
vals.Set("access_token", setting.Conf.Disqus.AccessToken)
vals.Set("forum", setting.Conf.Disqus.ShortName)
vals.Set("title", artc.Title+" | "+Ei.BTitle)
vals.Set("identifier", "post-"+artc.Slug)
urlPath := fmt.Sprintf("https://%s/post/%s.html", setting.Conf.Mode.Domain, artc.Slug)
vals.Set("url", urlPath)
resp, err := PostForm(setting.Conf.Disqus.ThreadCreate, vals)
if err != nil {
logd.Error(err)
return ""
return err
}
request.Header.Set("Referer", "https://disqus.com")
resp, err := http.DefaultClient.Do(request)
if err != nil {
logd.Error(err)
return ""
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
logd.Error(err)
return ""
return err
}
if resp.StatusCode != http.StatusOK {
logd.Error(string(b))
return ""
return errors.New(string(b))
}
pr := &PostResponse{}
err = json.Unmarshal(b, pr)
result := &threadCreateResp{}
err = json.Unmarshal(b, result)
if err != nil {
logd.Error(err)
return ""
return err
}
logd.Print(pr.Response.Id)
return pr.Response.Id
artc.Thread = result.Response.Id
return nil
}
///////////////////////////// HTTP 请求 /////////////////////////////
var httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
func newRequest(method, rawurl string, vals url.Values) (*http.Request, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
host := u.Host
// 获取主机IP
ips, err := net.LookupHost(u.Host)
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, errors.New("not found ip: " + u.Host)
}
// 设置ServerName
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
u.Host = ips[0]
// 创建HTTP Request
var req *http.Request
if vals != nil {
req, err = http.NewRequest(method, u.String(), strings.NewReader(vals.Encode()))
} else {
req, err = http.NewRequest(method, u.String(), nil)
}
if err != nil {
return nil, err
}
// 改变Host
req.Host = host
return req, nil
}
// Get HTTP Get请求
func Get(rawurl string) (*http.Response, error) {
req, err := newRequest(http.MethodGet, rawurl, nil)
if err != nil {
return nil, err
}
// 发起请求
return httpClient.Do(req)
}
// PostForm HTTP Post请求
func PostForm(rawurl string, vals url.Values) (*http.Response, error) {
req, err := newRequest(http.MethodPost, rawurl, vals)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// 发起请求
return httpClient.Do(req)
}
// PostWithHeader HTTP Post请求自定义Header
func PostWithHeader(rawurl string, vals url.Values, header http.Header) (*http.Response, error) {
req, err := newRequest(http.MethodPost, rawurl, vals)
if err != nil {
return nil, err
}
// set header
req.Header = header
// 发起请求
return httpClient.Do(req)
}

View File

@@ -8,18 +8,29 @@ func TestDisqus(t *testing.T) {
PostsCount()
}
func TestPostComment(t *testing.T) {
pc := &PostCreate{
func TestPostCreate(t *testing.T) {
pc := &PostComment{
Message: "hahahaha",
Thread: "52799014",
AuthorEmail: "deepzz.qi@gmail.com",
AuthorName: "deepzz",
}
id := PostComment(pc)
if id == "" {
t.Error("post failed")
id, err := PostCreate(pc)
if err != nil {
t.Error(err)
return
}
t.Log("post success")
t.Log("post success", id)
}
func TestThreadCreate(t *testing.T) {
tc := &Article{
Title: "测试test7",
Slug: "test7",
}
err := ThreadCreate(tc)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -25,8 +25,6 @@ services:
volumes:
- /data/eiblog/logdata:/eiblog/logdata
- /data/eiblog/conf:/eiblog/conf
- /data/eiblog/static:/eiblog/static
- /data/eiblog/views:/eiblog/views
links:
- elasticsearch
- mongodb
@@ -35,3 +33,14 @@ services:
ports:
- "9000:9000"
restart: always
# backup:
# image: registry.cn-hangzhou.aliyuncs.com/deepzz/backup
# container_name: backup
# links:
# - mongodb
# environment:
# - QINIU_BUCKET=xxxx
# - QINIU_DOMAIN=xx.example.com
# - ACCESS_KEY=xxxxxxxxxx
# - SECRECT_KEY=xxxxxxxxxx
# restart: always

1
docs/_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@@ -18,3 +18,7 @@ twitter:
![twitter-card2](http://7xokm2.com1.z0.glb.clouddn.com/img/twitter-pub2.png)
可以看到``之前是没有内容的,该内容是我们文章的描述。
### Google OpenSearch
在 Chrome 浏览器上,你可以在输入网站后按 TAB 键进入搜索模式,如:
![opensearch](http://7xokm2.com1.z0.glb.clouddn.com/opensearch.gif)

69
docs/autocert.md Normal file
View File

@@ -0,0 +1,69 @@
### 证书自动更新
本博客证书自动更新有两种方式:
* [acme/autocert](https://github.com/golang/crypto/tree/master/acme/autocert),博客内部集成,通过 tls-sni 验证,实现全自动更新证书,一键开启关闭。请在裸服务器的情况下使用(不要使用代理)。单证书
* [acme.sh](https://github.com/Neilpang/acme.sh),强大的 acme 脚本,多种自动更新证书方式,满足你各方面的需求。双证书
#### 方式一
什么是 autocert简单点你只需要两步操作
1. 将域名解析到你的服务器。
2. 在服务器上运行开启 autocert 功能的程序(这里不需要配置证书),需要占用 443 端口。
其它过程你不需要过问,即会完成自动申请证书,自动更新证书的功能(默认 30 天)。这个是在 tcp/ip 层的操作,对用户完全透明,非常棒。
一键开启 autocert 功能,只需修改 `conf/app.yml` 文件内容:
```
# 运行模式
mode:
# http server
enablehttp: true
httpport: 9000
# https server
enablehttps: true # 必须开启
autocert: false # autocert 功能开关
httpsport: 9001
certfile:
keyfile:
domain: deepzz.com # 申请证书的域名,也是博客的域名
```
首先,使用 HTTPS 必须启用 `enablehttps`,它有两个作用:
* 如果 `enablehttp` 开启,会自动 301 重定向到 https。
* 作为开启 autocert 的前提条件。
其次, `autocert` 是否开启也有两个作用:
* false服务器将使用 `httpsport``certfile``keyfile` 作为参数启动 https 服务器。
* true服务器直接使用 443 端口启动 https 服务器,并且自动申请证书,且在证书只有 30 天有效期时自动更新证书。域名为 *运行模式* 下的 mode->domain。
#### 方式二
使用方式二,你需要了解 acme.sh 的具体使用方式,非常简单。选择适合自己的方式实现自动更新证书。
博主,这里实现了 aliyun dns 的自动验证,自动更新证书。详情参见 Makefile->gencert。这里实现了自动申请 ecc、rsa 双证书,并且自动申请 scts自动安装自动更新。
基本流程如下:
1. 创建相关目录:`/data/eiblog/conf/ssl``/data/eiblog/conf/scts/rsa``/data/eiblog/conf/scts/ecc`
2. 自动下载安装 acme.sh 脚本。
3. 自动申请 RSA 证书并且自动获取 scts并且自动安装到指定位置。
4. 自动申请 ECC 证书并且自动获取 scts并且自动安装到指定位置。
##### 使用方式
导出环境变量Aliyun dns 的环境变量为:
```
export Ali_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
export Ali_Secret="jlsdflanljkljlfdsaklkjflsa"
```
执行命令:
```
$ make gencert -cn=common_name -sans="-d example.com -d example1.com"
```

View File

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

View File

@@ -59,3 +59,16 @@ 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

@@ -6,12 +6,12 @@ import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"regexp"
"strings"
"time"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
)
@@ -24,13 +24,25 @@ const (
ES_DATE = `{"range":{"date":{"gte":"%s","lte": "%s","format": "yyyy-MM-dd||yyyy-MM||yyyy"}}}` // 2016-10||/M
)
var es *ElasticService
var (
ErrUninitializedES = errors.New("uninitialized elasticsearch")
es *ElasticService
)
// 初始化 Elasticsearch 服务器
func init() {
es = &ElasticService{url: setting.Conf.SearchURL, c: new(http.Client)}
_, err := net.LookupIP("elasticsearch")
if err != nil {
logd.Info(err)
return
}
es = &ElasticService{url: "http://elasticsearch:9200", c: new(http.Client)}
initIndex()
}
// 创建索引
func initIndex() {
mappings := fmt.Sprintf(`{"mappings":{"%s":{"properties":{"content":{"analyzer":"ik_syno","search_analyzer":"ik_syno","term_vector":"with_positions_offsets","type":"string"},"date":{"index":"not_analyzed","type":"date"},"slug":{"type":"string"},"tag":{"index":"not_analyzed","type":"string"},"title":{"analyzer":"ik_syno","search_analyzer":"ik_syno","term_vector":"with_positions_offsets","type":"string"}}}}}`, TYPE)
err := CreateIndexAndMappings(INDEX, TYPE, []byte(mappings))
@@ -39,7 +51,12 @@ func initIndex() {
}
}
func Elasticsearch(qStr string, size, from int) *ESSearchResult {
// 查询
func Elasticsearch(qStr string, size, from int) (*ESSearchResult, error) {
if es == nil {
return nil, ErrUninitializedES
}
// 分析查询字符串
reg := regexp.MustCompile(`(tag|slug|date):`)
indexs := reg.FindAllStringIndex(qStr, -1)
@@ -90,13 +107,17 @@ func Elasticsearch(qStr string, size, from int) *ESSearchResult {
}
docs, err := IndexQueryDSL(INDEX, TYPE, size, from, []byte(dsl))
if err != nil {
logd.Error(err)
return nil
return nil, err
}
return docs
return docs, nil
}
// 添加或更新索引
func ElasticIndex(artc *Article) error {
if es == nil {
return ErrUninitializedES
}
img := PickFirstImage(artc.Content)
mapping := map[string]interface{}{
"title": artc.Title,
@@ -110,7 +131,12 @@ func ElasticIndex(artc *Article) error {
return IndexOrUpdateDocument(INDEX, TYPE, artc.ID, b)
}
// 删除索引
func ElasticDelIndex(ids []int32) error {
if es == nil {
return ErrUninitializedES
}
var target []string
for _, id := range ids {
target = append(target, fmt.Sprint(id))
@@ -128,10 +154,12 @@ type IndicesCreateResult struct {
Acknowledged bool `json:"acknowledged"`
}
// 返回 url
func (s *ElasticService) ParseURL(format string, params ...interface{}) string {
return fmt.Sprintf(s.url+format, params...)
}
// Elastic 相关操作请求
func (s *ElasticService) Do(req *http.Request) (interface{}, error) {
resp, err := s.c.Do(req)
if err != nil {
@@ -153,11 +181,8 @@ func (s *ElasticService) Do(req *http.Request) (interface{}, error) {
return b, nil
case "HEAD":
return resp.StatusCode, nil
default:
return nil, errors.New("unknown methods")
}
return nil, nil
return nil, errors.New("unknown methods")
}
func CreateIndexAndMappings(index, typ string, mappings []byte) (err error) {
@@ -188,6 +213,7 @@ func CreateIndexAndMappings(index, typ string, mappings []byte) (err error) {
return nil
}
// 创建或更新索引
func IndexOrUpdateDocument(index, typ string, id int32, doc []byte) (err error) {
req, err := http.NewRequest("PUT", es.ParseURL("/%s/%s/%d", index, typ, id), bytes.NewReader(doc))
if err != nil {
@@ -214,6 +240,7 @@ type ESDeleteResult struct {
} `json:"iterms"`
}
// 删除文档
func DeleteDocument(index, typ string, ids []string) error {
var buff bytes.Buffer
for _, id := range ids {
@@ -248,6 +275,7 @@ func DeleteDocument(index, typ string, ids []string) error {
return nil
}
// 查询结果
type ESSearchResult struct {
Took float32 `json:"took"`
Hits struct {
@@ -269,6 +297,7 @@ type ESSearchResult struct {
} `json:"hits"`
}
// DSL 语句查询文档
func IndexQueryDSL(index, typ string, size, from int, dsl []byte) (*ESSearchResult, error) {
req, err := http.NewRequest("POST", es.ParseURL("/%s/%s/_search?size=%d&from=%d", index, typ, size, from), bytes.NewReader(dsl))
if err != nil {

163
front.go
View File

@@ -20,13 +20,46 @@ import (
func Filter() gin.HandlerFunc {
return func(c *gin.Context) {
// 过滤黑名单
BlackFilter(c)
if BlackFilter(c) {
c.Abort()
return
}
// 重定向
if Redirect(c) {
c.Abort()
return
}
// 用户cookie用于统计
UserCookie(c)
c.Next()
}
}
// 黑名单过滤
func BlackFilter(c *gin.Context) bool {
ip := c.ClientIP()
if setting.BlackIP[ip] {
c.String(http.StatusForbidden, "Your IP is blacklisted.")
return true
}
return false
}
// 重定向
func Redirect(c *gin.Context) bool {
if setting.Conf.Mode.EnableHttps && c.Request.ProtoMajor == 1 {
var port string
if strings.Contains(c.Request.Host, ":") {
port = fmt.Sprintf(":%d", setting.Conf.Mode.HttpsPort)
}
c.Redirect(http.StatusMovedPermanently, "https://"+setting.Conf.Mode.Domain+port+c.Request.RequestURI)
return true
}
return false
}
// 用户识别
func UserCookie(c *gin.Context) {
cookie, err := c.Cookie("u")
@@ -35,15 +68,6 @@ func UserCookie(c *gin.Context) {
}
}
// 黑名单过滤
func BlackFilter(c *gin.Context) {
ip := c.ClientIP()
if setting.BlackIP[ip] {
c.Abort()
c.String(http.StatusForbidden, "Your IP is blacklisted.")
}
}
// 解析静态文件版本
func StaticVersion(c *gin.Context) (version int) {
cookie, err := c.Request.Cookie("v")
@@ -55,7 +79,6 @@ func StaticVersion(c *gin.Context) (version int) {
func GetBase() gin.H {
return gin.H{
"Favicon": setting.Conf.Favicon,
"BlogName": Ei.BlogName,
"SubTitle": Ei.SubTitle,
"Twitter": setting.Conf.Twitter,
@@ -63,10 +86,12 @@ func GetBase() gin.H {
"BTitle": Ei.BTitle,
"BeiAn": Ei.BeiAn,
"Domain": setting.Conf.Mode.Domain,
"Kodo": setting.Conf.Kodo,
"Qiniu": setting.Conf.Qiniu,
"Disqus": setting.Conf.Disqus,
}
}
// not found
func HandleNotFound(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -77,6 +102,7 @@ func HandleNotFound(c *gin.Context) {
RenderHTMLFront(c, "notfound", h)
}
// 首页
func HandleHomePage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -88,11 +114,12 @@ func HandleHomePage(c *gin.Context) {
if err != nil || pn < 1 {
pn = 1
}
h["Prev"], h["Next"], h["List"] = PageList(pn, setting.Conf.PageNum)
h["Prev"], h["Next"], h["List"] = PageList(pn, setting.Conf.General.PageNum)
c.Status(http.StatusOK)
RenderHTMLFront(c, "home", h)
}
// 专题页
func HandleSeriesPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -105,6 +132,7 @@ func HandleSeriesPage(c *gin.Context) {
RenderHTMLFront(c, "series", h)
}
// 归档页
func HandleArchivesPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -117,6 +145,7 @@ func HandleArchivesPage(c *gin.Context) {
RenderHTMLFront(c, "archives", h)
}
// 文章
func HandleArticlePage(c *gin.Context) {
path := c.Param("slug")
if !strings.HasSuffix(path, ".html") || Ei.MapArticles[path[:len(path)-5]] == nil {
@@ -154,6 +183,7 @@ func HandleArticlePage(c *gin.Context) {
RenderHTMLFront(c, name, h)
}
// 搜索页
func HandleSearchPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -169,10 +199,12 @@ func HandleSearchPage(c *gin.Context) {
start = 1
}
h["Word"] = q
var result *ESSearchResult
vals := c.Request.URL.Query()
result = Elasticsearch(q, setting.Conf.PageNum, start-1)
if result != nil {
result, err := Elasticsearch(q, setting.Conf.General.PageNum, start-1)
if err != nil {
logd.Error(err)
} else {
result.Took /= 1000
for i, v := range result.Hits.Hits {
if artc := Ei.MapArticles[result.Hits.Hits[i].Source.Slug]; len(v.Highlight.Content) == 0 && artc != nil {
@@ -180,12 +212,12 @@ func HandleSearchPage(c *gin.Context) {
}
}
h["SearchResult"] = result
if start-setting.Conf.PageNum > 0 {
vals.Set("start", fmt.Sprint(start-setting.Conf.PageNum))
if start-setting.Conf.General.PageNum > 0 {
vals.Set("start", fmt.Sprint(start-setting.Conf.General.PageNum))
h["Prev"] = vals.Encode()
}
if result.Hits.Total >= start+setting.Conf.PageNum {
vals.Set("start", fmt.Sprint(start+setting.Conf.PageNum))
if result.Hits.Total >= start+setting.Conf.General.PageNum {
vals.Set("start", fmt.Sprint(start+setting.Conf.General.PageNum))
h["Next"] = vals.Encode()
}
}
@@ -196,6 +228,7 @@ func HandleSearchPage(c *gin.Context) {
RenderHTMLFront(c, "search", h)
}
// 评论页
func HandleDisqusFrom(c *gin.Context) {
params := strings.Split(c.Param("slug"), "|")
if len(params) != 4 || params[1] == "" {
@@ -207,6 +240,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 {
@@ -215,22 +249,36 @@ func HandleDisqusFrom(c *gin.Context) {
c.Header("Content-Type", "text/html; charset=utf-8")
}
// feed
func HandleFeed(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "static/feed.xml")
}
// opensearch
func HandleOpenSearch(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "static/opensearch.xml")
}
// robots
func HandleRobots(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "static/robots.txt")
}
// sitemap
func HandleSitemap(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "static/sitemap.xml")
}
// cross domain
func HandleCrossDomain(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "static/crossdomain.xml")
}
// favicon
func HandleFavicon(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "static/favicon.ico")
}
// 服务端推送谷歌统计
func HandleBeacon(c *gin.Context) {
ua := c.Request.UserAgent()
@@ -245,7 +293,7 @@ func HandleBeacon(c *gin.Context) {
vals.Set("dl", c.Request.Referer())
vals.Set("uip", c.ClientIP())
go func() {
req, err := http.NewRequest("POST", "https://www.google-analytics.com/collect", strings.NewReader(vals.Encode()))
req, err := http.NewRequest("POST", setting.Conf.Google.URL, strings.NewReader(vals.Encode()))
if err != nil {
logd.Error(err)
return
@@ -276,7 +324,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"`
@@ -288,17 +336,25 @@ type commentsDetail struct {
Name string `json:"name"`
Url string `json:"url"`
Avatar string `json:"avatar"`
CreatedAt string `json:"createdAt"`
CreatedAtStr string `json:"createdAtStr"`
Message string `json:"message"`
IsDeleted bool `json:"isDeleted"`
}
func HandleDisqus(c *gin.Context) {
slug := c.Param("slug")
cursor := c.Query("cursor")
dcs := DisqusComments{}
postsList := PostsList(slug, cursor)
if postsList != nil {
if artc := Ei.MapArticles[slug]; artc != nil {
dcs.Data.Thread = artc.Thread
}
postsList, err := PostsList(slug, cursor)
if err != nil {
logd.Error(err)
dcs.ErrNo = FAIL
dcs.ErrMsg = "系统错误"
} else {
dcs.ErrNo = postsList.Code
if postsList.Cursor.HasNext {
dcs.Data.Next = postsList.Cursor.Next
@@ -315,33 +371,40 @@ func HandleDisqus(c *gin.Context) {
Parent: v.Parent,
Url: v.Author.ProfileUrl,
Avatar: v.Author.Avatar.Cache,
CreatedAt: v.CreatedAt,
CreatedAtStr: ConvertStr(v.CreatedAt),
Message: v.Message,
IsDeleted: v.IsDeleted,
}
}
} else {
dcs.ErrNo = FAIL
dcs.ErrMsg = "系统错误"
}
c.JSON(http.StatusOK, dcs)
}
// [thread:[5279901489] parent:[] identifier:[post-troubleshooting-https] next:[] author_name:[你好] author_email:[chenqijing2@163.com] message:[fdsfdsf]]
// 发表评论
// [thread:[5279901489] parent:[] identifier:[post-troubleshooting-https]
// next:[] author_name:[你好] author_email:[chenqijing2@163.com] message:[fdsfdsf]]
type DisqusCreate struct {
ErrNo int `json:"errno"`
ErrMsg string `json:"errmsg"`
Data commentsDetail `json:"data"`
}
func HandleDisqusCreate(c *gin.Context) {
rep := gin.H{"errno": SUCCESS, "errmsg": ""}
defer c.JSON(http.StatusOK, rep)
resp := &DisqusCreate{}
defer c.JSON(http.StatusOK, resp)
msg := c.PostForm("message")
email := c.PostForm("author_email")
name := c.PostForm("author_name")
thread := c.PostForm("thread")
identifier := c.PostForm("identifier")
if msg == "" || email == "" || name == "" || thread == "" || identifier == "" {
rep["errno"] = FAIL
rep["errmsg"] = "参数错误"
resp.ErrNo = FAIL
resp.ErrMsg = "参数错误"
return
}
pc := &PostCreate{
fmt.Println("disqus: author: ", email)
pc := &PostComment{
Message: msg,
Parent: c.PostForm("parent"),
Thread: thread,
@@ -351,16 +414,34 @@ func HandleDisqusCreate(c *gin.Context) {
IpAddress: c.ClientIP(),
}
id := PostComment(pc)
if id == "" {
rep["errno"] = FAIL
rep["errmsg"] = "系统错误"
postDetail, err := PostCreate(pc)
if err != nil {
logd.Error(err)
resp.ErrNo = FAIL
resp.ErrMsg = "系统错误"
return
}
rep["errno"] = SUCCESS
rep["data"] = gin.H{"id": id}
err = PostApprove(postDetail.Response.Id)
if err != nil {
logd.Error(err)
resp.ErrNo = FAIL
resp.ErrMsg = "系统错误"
return
}
resp.ErrNo = SUCCESS
resp.Data = commentsDetail{
Id: postDetail.Response.Id,
Name: name,
Parent: postDetail.Response.Parent,
Url: postDetail.Response.Author.ProfileUrl,
Avatar: postDetail.Response.Author.Avatar.Cache,
CreatedAtStr: ConvertStr(postDetail.Response.CreatedAt),
Message: postDetail.Response.Message,
IsDeleted: postDetail.Response.IsDeleted,
}
}
// 渲染页面
func RenderHTMLFront(c *gin.Context, name string, data gin.H) {
var buf bytes.Buffer
err := Tmpl.ExecuteTemplate(&buf, name, data)

83
glide.lock generated
View File

@@ -1,83 +0,0 @@
hash: bd360fa297ed66950543990f9433cdcdf13c29dd99d9a01b49027e236b2cb9da
updated: 2017-03-11T00:26:32.776134316+08:00
imports:
- name: github.com/boj/redistore
version: 3f631e1df8711f449f7b1796f5b8b7716e4319d0
- name: github.com/eiblog/blackfriday
version: c0ec111761ae784fe31cc076f2fa0e2d2216d623
- name: github.com/eiblog/utils
version: ad2f63940c4f16d0dbfc3f4df59e8cb7af0f80ec
subpackages:
- logd
- mgo
- tmpl
- uuid
- name: github.com/garyburd/redigo
version: 0d253a66e6e1349f4581d6d2b300ee434ee2da9f
subpackages:
- internal
- redis
- name: github.com/gin-gonic/contrib
version: ffa12cf90b52a4af3ae604f6452e61f8218a517c
subpackages:
- sessions
- name: github.com/gin-gonic/gin
version: e2212d40c62a98b388a5eb48ecbdcf88534688ba
subpackages:
- binding
- render
- name: github.com/golang/protobuf
version: 2402d76f3d41f928c7902a765dfc872356dd3aad
subpackages:
- proto
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/securecookie
version: e59506cc896acb7f7bf732d4fdf5e25f7ccd8983
- name: github.com/gorilla/sessions
version: ba78acc856fe7c79891d516131b3d903cc8d9367
- name: github.com/manucorporat/sse
version: ee05b128a739a0fb76c7ebd3ae4810c1de808d6d
- name: github.com/mattn/go-isatty
version: 57fdcb988a5c543893cc61bce354a6e24ab70022
- name: github.com/shurcooL/sanitized_anchor_name
version: 1dba4b3954bc059efc3991ec364f9f9a35f597d2
- name: golang.org/x/net
version: f315505cf3349909cdf013ea56690da34e96a451
subpackages:
- context
- name: golang.org/x/sys
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
subpackages:
- unix
- name: gopkg.in/go-playground/validator.v8
version: c193cecd124b5cc722d7ee5538e945bdb3348435
- name: gopkg.in/mgo.v2
version: 3f83fa5005286a7fe593b055f0d7771a7dce4655
subpackages:
- bson
- internal/json
- internal/sasl
- internal/scram
- name: gopkg.in/yaml.v2
version: a3f3340b5840cee44f372bddb5880fcbc419b46a
- name: qiniupkg.com/api.v7
version: 427efe34a76257db04b3b785e3b4d2fdb44cf9b9
subpackages:
- api
- auth/qbox
- conf
- kodo
- kodocli
- name: qiniupkg.com/x
version: f512abcf45ab4e2ba0fd4784c57b53d495997d66
subpackages:
- bytes.v7
- bytes.v7/seekable
- ctype.v7
- log.v7
- reqid.v7
- rpc.v7
- url.v7
- xlog.v7
testImports: []

View File

@@ -1,25 +0,0 @@
package: github.com/eiblog/eiblog
import:
- package: github.com/eiblog/blackfriday
- package: github.com/eiblog/utils
subpackages:
- 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

28
go.mod Normal file
View File

@@ -0,0 +1,28 @@
module github.com/eiblog/eiblog
require (
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/deepzz0/logd v0.0.0-20171206094927-f91dd8c6316f
github.com/eiblog/blackfriday v0.0.0-20161010144836-c0ec111761ae
github.com/eiblog/utils v0.0.0-20181119015747-92c93e218753
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b
github.com/gin-gonic/contrib v0.0.0-20180614032058-39cfb9727134
github.com/gin-gonic/gin v1.3.0
github.com/golang/protobuf v1.4.3 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/qiniu/api.v7/v7 v7.8.0
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/stretchr/testify v1.3.0
github.com/ugorji/go v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
gopkg.in/yaml.v2 v2.2.1
)
go 1.13

99
go.sum Normal file
View File

@@ -0,0 +1,99 @@
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepzz0/logd v0.0.0-20171206094927-f91dd8c6316f h1:hjWy8ptp0ggYgv/3A8dixSB9KTRgDcZH2D3Ap5hrwOs=
github.com/deepzz0/logd v0.0.0-20171206094927-f91dd8c6316f/go.mod h1:8jMj6ab9czIU5udq3ovaK9/5sCIyQ1JWteFMn8w2QRI=
github.com/eiblog/blackfriday v0.0.0-20161010144836-c0ec111761ae h1:V6YC640Gs5jEUYfCimyuXsTW5gzNcIEESG4MGmOJCtA=
github.com/eiblog/blackfriday v0.0.0-20161010144836-c0ec111761ae/go.mod h1:HzHqTCGEAkSSzBM3shBvQHsHRQYUvjNOIC4mHipZ6tI=
github.com/eiblog/utils v0.0.0-20181119015747-92c93e218753 h1:Nygjtnh1nF5zejJF7pZnsoFh77wOPS+jlfhikjkJg60=
github.com/eiblog/utils v0.0.0-20181119015747-92c93e218753/go.mod h1:mZHWnipRp41yw/rti2DgpbMiBE5i6ifqg7PKooEwRh4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b h1:dm/NYytoj7p8Jc6zMvyRz3PCQrTTCXnVRvEzyBcM890=
github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b/go.mod h1:vwfeXwKgEIWq63oVfwaBjoByS4dZzYbHHROHjV4IjNY=
github.com/gin-gonic/contrib v0.0.0-20180614032058-39cfb9727134 h1:xgqFZVwmmtWiuq5LUZ/wa34hJR2Dm9NZAH+Cj9a7Hu0=
github.com/gin-gonic/contrib v0.0.0-20180614032058-39cfb9727134/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qiniu/api.v7/v7 v7.8.0 h1:Ye9sHXwCpeDgKJ4BNSoDvXe4yEuU8a/HTT1jKRgkqe8=
github.com/qiniu/api.v7/v7 v7.8.0/go.mod h1:J7pD9UsnxO7XxyRLUHpsWEQd/HgWJNwnn/Za9qEPdEA=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.2.2 h1:60ZHIOcsJlo3bJm9CbTVu7OSqT2mxaEmyQbK2NwCkn0=
github.com/ugorji/go v1.2.2/go.mod h1:bitgyERdV7L7Db/Z5gfd5v2NQMNhhiFiZwpgMw2SP7k=
github.com/ugorji/go/codec v1.2.2 h1:08Gah8d+dXj4cZNUHhtuD/S4PXD5WpVbj5B8/ClELAQ=
github.com/ugorji/go/codec v1.2.2/go.mod h1:OM8g7OAy52uYl3Yk+RE/3AS1nXFn1Wh4PPLtupCxbuU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -18,6 +18,7 @@ const (
FAIL
)
// 月份转换
var monthToDays = map[time.Month]int{
time.January: 31,
time.February: 28,
@@ -43,14 +44,17 @@ func EncryptPasswd(name, pass string) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
// 验证密码
func VerifyPasswd(origin, name, input string) bool {
return origin == EncryptPasswd(name, input)
}
// 随机 uuid
func RandUUIDv4() string {
return uuid.NewV4().String()
}
// 读取目录
func ReadDir(dir string, filter func(name string) bool) (files []string) {
fis, err := ioutil.ReadDir(dir)
if err != nil {
@@ -69,16 +73,18 @@ func ReadDir(dir string, filter func(name string) bool) (files []string) {
return
}
// 去掉 html tag
func IgnoreHtmlTag(src string) string {
//去除所有尖括号内的HTML代码
re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
// 去除所有尖括号内的HTML代码
re, _ := regexp.Compile(`<[\S\s]+?>`)
src = re.ReplaceAllString(src, "")
//去除换行符
re, _ = regexp.Compile("\\s{2,}")
// 去除换行符
re, _ = regexp.Compile(`\s+`)
return re.ReplaceAllString(src, "")
}
// 获取第一张图片
func PickFirstImage(html string) string {
re, _ := regexp.Compile(`data-src="(.*?)"`)
sli := re.FindAllStringSubmatch(html, 1)
@@ -98,15 +104,16 @@ const (
YEARS_AGO = "%d年前"
)
// 时间转换为间隔
func ConvertStr(str string) string {
t, err := time.ParseInLocation("2006-01-02T15:04:05", str, time.UTC)
if err != nil {
logd.Error(err, str)
return JUST_NOW
}
now := time.Now()
now := time.Now().UTC()
y1, m1, d1 := t.Date()
y2, m2, d2 := now.UTC().Date()
y2, m2, d2 := now.Date()
h1, mi1, s1 := t.Clock()
h2, mi2, s2 := now.Clock()
if y := y2 - y1; y > 1 || (y == 1 && m2-m1 >= 0) {
@@ -118,11 +125,12 @@ func ConvertStr(str string) string {
} else if h := d*24 + h2 - h1; h > 1 || (h == 1 && mi2-mi1 >= 0) {
return fmt.Sprintf(HOURS_AGO, h)
} else if mi := h*60 + mi2 - mi1; mi > 1 || (mi == 1 && s2-s1 >= 0) {
return fmt.Sprintf(MINUTES_AGO, m)
return fmt.Sprintf(MINUTES_AGO, mi)
}
return JUST_NOW
}
// 获取天数
func dayIn(year int, m time.Month) int {
if m == time.February && isLeap(year) {
return 29
@@ -130,6 +138,7 @@ func dayIn(year int, m time.Month) int {
return monthToDays[m]
}
// 是否是闰年
func isLeap(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}

64
helper_test.go Normal file
View File

@@ -0,0 +1,64 @@
// Package main provides ...
package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestReadDir(t *testing.T) {
files := ReadDir("setting", func(name string) bool { return false })
assert.Len(t, files, 2)
}
func TestIgnoreHtmlTag(t *testing.T) {
testStr := []string{
"<script>hello</script>",
}
expectStr := []string{
"hello",
}
for i, v := range testStr {
assert.Equal(t, expectStr[i], IgnoreHtmlTag(v))
}
}
func TestPickFirstImage(t *testing.T) {
testStr := []string{
`<img width="480" height="310" alt="acme_aliyun_1" src="https://st.deepzz.com/blog/img/acme_aliyun_1.jpg">`,
`<img width="480" height="310" alt="acme_aliyun_1" data-src="https://st.deepzz.com/acme_aliyun_1.jpg"><img width="480" height="310" alt="acme_aliyun_1" src="https://st.deepzz.com/blog/img/acme_aliyun_1.jpg">`,
}
expectStr := []string{
"",
"https://st.deepzz.com/acme_aliyun_1.jpg",
}
for i, v := range testStr {
assert.Equal(t, expectStr[i], PickFirstImage(v))
}
}
func TestCovertStr(t *testing.T) {
now := time.Now().UTC()
testStr := []string{
now.Format("2006-01-02T15:04:05"),
now.Add(-time.Second * 20).Format("2006-01-02T15:04:05"),
now.Add(-time.Minute).Format("2006-01-02T15:04:05"),
now.Add(-time.Minute * 2).Format("2006-01-02T15:04:05"),
now.Add(-time.Minute * 20).Format("2006-01-02T15:04:05"),
now.Add(-time.Hour).Format("2006-01-02T15:04:05"),
now.Add(-time.Hour * 2).Format("2006-01-02T15:04:05"),
now.Add(-time.Hour * 24).Format("2006-01-02T15:04:05"),
}
time.Sleep(time.Second)
t.Log(now.Format("2006-01-02T15:04:05"))
for _, v := range testStr {
t.Log(v, ConvertStr(v))
}
}

View File

@@ -121,6 +121,8 @@ type Article struct {
Excerpt string `bson:"-"`
// 一句话描述,文章第一句
Desc string `bson:"-"`
// disqus thread
Thread string `bson:"-"`
}
type SortArticles []*Article

View File

@@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
@@ -64,7 +65,7 @@ func (p *pingRPC) PingFunc(slug string) {
if len(setting.Conf.PingRPCs) == 0 {
return
}
p.Params.Param[1].Value = "https://" + setting.Conf.Mode.Domain + "/post/" + slug + ".html"
p.Params.Param[1].Value = fmt.Sprintf("https://%s/post/%s.html", setting.Conf.Mode.Domain, slug)
buf := &bytes.Buffer{}
buf.WriteString(xml.Header)
enc := xml.NewEncoder(buf)
@@ -105,6 +106,7 @@ func init() {
Pings = append(Pings, pr)
}
// ping
func DoPings(slug string) {
for _, p := range Pings {
go p.PingFunc(slug)

View File

@@ -1,114 +1,103 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"github.com/eiblog/eiblog/setting"
"qiniupkg.com/api.v7/kodo"
"qiniupkg.com/api.v7/kodocli"
url "qiniupkg.com/x/url.v7"
"github.com/qiniu/api.v7/v7/auth/qbox"
"github.com/qiniu/api.v7/v7/storage"
)
var qiniu_cfg = &kodo.Config{
AccessKey: setting.Conf.Kodo.AccessKey,
SecretKey: setting.Conf.Kodo.SecretKey,
Scheme: "https",
}
type bucket struct {
name string
domain string
accessKey string
secretKey string
}
type PutRet struct {
Hash string `json:"hash"`
Key string `json:"key"`
}
// 进度条
func onProgress(fsize, uploaded int64) {
d := int(float64(uploaded) / float64(fsize) * 100)
if fsize == uploaded {
fmt.Printf("\rUpload completed! ")
fmt.Printf("\rUpload completed! \n")
} else {
fmt.Printf("\r%02d%% uploaded ", int(d))
}
}
// 上传文件
func FileUpload(name string, size int64, data io.Reader) (string, error) {
if setting.Conf.Kodo.AccessKey == "" || setting.Conf.Kodo.SecretKey == "" {
if setting.Conf.Qiniu.AccessKey == "" || setting.Conf.Qiniu.SecretKey == "" {
return "", errors.New("qiniu config error")
}
// 创建一个client
c := kodo.New(0, qiniu_cfg)
key := getKey(name)
mac := qbox.NewMac(setting.Conf.Qiniu.AccessKey, setting.Conf.Qiniu.SecretKey)
// 设置上传的策略
policy := &kodo.PutPolicy{
Scope: setting.Conf.Kodo.Name,
putPolicy := &storage.PutPolicy{
Scope: setting.Conf.Qiniu.Bucket,
Expires: 3600,
InsertOnly: 1,
}
// 上传token
upToken := putPolicy.UploadToken(mac)
// 生成一个上传token
token := c.MakeUptoken(policy)
// 构建一个uploader
zone := 0
uploader := kodocli.NewUploader(zone, nil)
key := getKey(name)
if key == "" {
return "", errors.New("不支持的文件类型")
// 上传配置
cfg := &storage.Config{
Zone: &storage.ZoneHuadong,
UseHTTPS: true,
}
// uploader
uploader := storage.NewFormUploader(cfg)
ret := new(storage.PutRet)
putExtra := &storage.PutExtra{}
var ret PutRet
var extra = kodocli.PutExtra{OnProgress: onProgress}
err := uploader.Put(nil, &ret, token, key, data, size, &extra)
err := uploader.Put(context.Background(), ret, upToken, key, data, size, putExtra)
if err != nil {
return "", err
}
url := "https://" + setting.Conf.Kodo.Domain + "/" + url.Escape(key)
url := "https://" + setting.Conf.Qiniu.Domain + "/" + key
return url, nil
}
// 删除文件
func FileDelete(name string) error {
// new一个Bucket管理对象
c := kodo.New(0, qiniu_cfg)
p := c.Bucket(setting.Conf.Kodo.Name)
key := getKey(name)
if key == "" {
return errors.New("不支持的文件类型")
}
// 调用Delete方法删除文件
err := p.Delete(nil, key)
// 打印返回值以及出错信息
mac := qbox.NewMac(setting.Conf.Qiniu.AccessKey, setting.Conf.Qiniu.SecretKey)
// 上传配置
cfg := &storage.Config{
Zone: &storage.ZoneHuadong,
UseHTTPS: true,
}
// manager
bucketManager := storage.NewBucketManager(mac, cfg)
// Delete
err := bucketManager.Delete(setting.Conf.Qiniu.Bucket, key)
if err != nil {
return err
}
return nil
}
// 修复路径
func getKey(name string) string {
ext := filepath.Ext(name)
var key string
switch ext {
case ".bmp", ".png", ".jpg", ".gif", ".ico":
case ".bmp", ".png", ".jpg", ".gif", ".ico", ".jpeg":
key = "blog/img/" + name
case ".mov", ".mp4":
key = "blog/video/" + name
case ".go", ".js", ".css", ".cpp", ".php", ".rb", ".java", ".py", ".sql", ".lua", ".html", ".sh", ".xml", ".cs":
case ".go", ".js", ".css", ".cpp", ".php", ".rb",
".java", ".py", ".sql", ".lua", ".html",
".sh", ".xml", ".cs":
key = "blog/code/" + name
case ".txt", ".md", ".ini", ".yaml", ".yml", ".doc", ".ppt", ".pdf":
case ".txt", ".md", ".ini", ".yaml", ".yml",
".doc", ".ppt", ".pdf":
key = "blog/document/" + name
case ".zip", ".rar", ".tar", ".gz":
key = "blog/archive/" + name
default:
key = "blog/other/" + name
}
return key
}

View File

@@ -7,11 +7,13 @@ import (
)
func TestUpload(t *testing.T) {
path := "/Users/chen/Desktop/png-MicroService-by-StuQ.png"
path := "qiniu.go"
file, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer file.Close()
info, _ := file.Stat()
url, err := FileUpload(info.Name(), info.Size(), file)
if err != nil {

View File

@@ -2,13 +2,15 @@
package main
import (
"crypto/rand"
"fmt"
"html/template"
"text/template"
"time"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
"github.com/eiblog/utils/tmpl"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
)
@@ -19,12 +21,19 @@ var (
)
func init() {
// 运行模式
if setting.Conf.RunMode == setting.PROD {
gin.SetMode(gin.ReleaseMode)
logd.SetLevel(logd.Lerror)
}
router = gin.Default()
store := sessions.NewCookieStore([]byte("eiblog321"))
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
logd.Fatal(err)
}
store := sessions.NewCookieStore(b)
store.Options(sessions.Options{
MaxAge: 86400 * 7,
Path: "/",
@@ -40,7 +49,7 @@ func init() {
}
return false
})
_, err := Tmpl.ParseFiles(files...)
_, err = Tmpl.ParseFiles(files...)
if err != nil {
logd.Fatal(err)
}
@@ -62,6 +71,8 @@ func init() {
router.GET("/opensearch.xml", HandleOpenSearch)
router.GET("/sitemap.xml", HandleSitemap)
router.GET("/robots.txt", HandleRobots)
router.GET("/crossdomain.xml", HandleCrossDomain)
router.GET("/favicon.ico", HandleFavicon)
// 后台相关
admin := router.Group("/admin")
admin.GET("/login", HandleLogin)
@@ -87,6 +98,7 @@ func init() {
}
}
// 开始运行
func Run() {
var (
endRunning = make(chan bool, 1)
@@ -94,25 +106,38 @@ func Run() {
)
if setting.Conf.Mode.EnableHttp {
go func() {
logd.Infof("http server Running on %d\n", setting.Conf.Mode.HttpPort)
logd.Printf("http server Running on %d\n", setting.Conf.Mode.HttpPort)
err = router.Run(fmt.Sprintf(":%d", setting.Conf.Mode.HttpPort))
if err != nil {
logd.Info("ListenAndServe: ", err)
logd.Error("ListenAndServe: ", err)
time.Sleep(100 * time.Microsecond)
endRunning <- true
}
}()
}
if setting.Conf.Mode.EnableHttps {
go func() {
logd.Infof("https server Running on %d\n", setting.Conf.Mode.HttpsPort)
err = router.RunTLS(fmt.Sprintf(":%d", setting.Conf.Mode.HttpsPort), setting.Conf.Mode.CertFile, setting.Conf.Mode.KeyFile)
if err != nil {
logd.Info("ListenAndServe: ", err)
time.Sleep(100 * time.Microsecond)
endRunning <- true
}
}()
if setting.Conf.Mode.AutoCert {
go func() {
logd.Print("https server Running on 443")
err = autotls.Run(router, setting.Conf.Mode.Domain)
if err != nil {
logd.Error("ListenAndServe: ", err)
time.Sleep(100 * time.Microsecond)
endRunning <- true
}
}()
} else {
go func() {
logd.Printf("https server Running on %d\n", setting.Conf.Mode.HttpsPort)
err = router.RunTLS(fmt.Sprintf(":%d", setting.Conf.Mode.HttpsPort),
setting.Conf.Mode.CertFile, setting.Conf.Mode.KeyFile)
if err != nil {
logd.Error("ListenAndServe: ", err)
time.Sleep(100 * time.Microsecond)
endRunning <- true
}
}()
}
}
<-endRunning
}

View File

@@ -9,8 +9,8 @@ import (
)
const (
DEV = "dev"
PROD = "prod"
DEV = "dev" // 该模式会输出 debug 等信息
PROD = "prod" // 该模式用于生产环境
)
var (
@@ -19,55 +19,68 @@ var (
)
type Config struct {
StaticVersion int // 当前静态文件版本
RunMode string // 运行模式
Trash int // 回收箱文章保留时间
Clean int // 清理回收箱频率
PageNum int // 前端每页文章数量
PageSize int // 后台每页文章数量
Length int // 自动截取预览长度
Identifier string // 截取标示
Description string // 文章描述前缀
Favicon string // icon地址
StartID int32 // 文章起始id
SearchURL string // elasticsearch 地址
Disqus struct { // 获取文章数量相关
ShortName string
PublicKey string
PostsCount string
PostsList string
PostCreate string
Interval int
StaticVersion int // 当前静态文件版本
FeedrURL string // superfeedr url
HotWords []string // 热搜词
PingRPCs []string // ping rpc 地址
General struct {
PageNum int // 前端每页文章数量
PageSize int // 后台每页文章数量
StartID int32 // 文章起始id
DescPrefix string // 文章描述前缀
Identifier string // 文章截取标示
Length int // 文章自动截取预览长度
Trash int // 回收箱文章保留时间
Clean int // 清理回收箱频率
}
HotWords []string // 热搜词
Google struct { // 谷歌统计
Disqus struct { // 获取文章数量相关
ShortName string
PublicKey string
AccessToken string
PostsCount string
PostsList string
PostCreate string
PostApprove string
ThreadCreate string
Interval int
}
Google struct { // 谷歌统计
URL string
Tid string
V string
T string
}
Kodo struct { // 七牛CDN
Name string
Qiniu struct { // 七牛CDN
Bucket string
Domain string
AccessKey string
SecretKey string
}
Mode RunMode // 运行模式
Mode struct { // 运行模式
EnableHttp bool
HttpPort int
EnableHttps bool
HttpsPort int
CertFile string
KeyFile string
AutoCert bool
Domain string
}
Twitter struct { // twitter信息
Card string
Site string
Image string
Address string
}
FeedrURL string // superfeedr url
PingRPCs []string // ping rpc 地址
Account struct {
Account struct { // account 账户
Username string // *
Password string // *
Email string
PhoneNumber string
Address string
}
Blogger struct { // 初始化数据
Blogger struct { // blog info 博客信息
BlogName string
SubTitle string
BeiAn string
@@ -76,16 +89,6 @@ type Config struct {
}
}
type RunMode struct {
EnableHttp bool
HttpPort int
EnableHttps bool
HttpsPort int
CertFile string
KeyFile string
Domain string
}
func init() {
// 初始化配置
data, err := ioutil.ReadFile("conf/app.yml")

View File

@@ -2,11 +2,11 @@
package setting
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
init()
fmt.Printf("%v\n", *Conf)
assert.NotNil(t, Conf)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

25
vendor/github.com/boj/redistore/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,25 @@
language: go
sudo: false
services:
- redis-server
matrix:
include:
- go: 1.5
- go: 1.6
- go: 1.7
- go: 1.8
- go: 1.9
- go: tip
allow_failures:
- go: tip
install:
- # skip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go tool vet .
- go test -v -race ./...

View File

@@ -1,12 +1,13 @@
# redistore
[![Build Status](https://drone.io/github.com/boj/redistore/status.png)](https://drone.io/github.com/boj/redistore/latest)
[![GoDoc](https://godoc.org/github.com/boj/redistore?status.svg)](https://godoc.org/github.com/boj/redistore)
[![Build Status](https://travis-ci.org/boj/redistore.svg?branch=master)](https://travis-ci.org/boj/redistore)
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.
Depends on the [Redigo](https://github.com/gomodule/redigo) Redis library.
## Installation
@@ -19,34 +20,34 @@ 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()
// 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())
}
// Get a session.
session, err = store.Get(req, "session-key")
if err != nil {
log.Error(err.Error())
}
// Add a value.
session.Values["foo"] = "bar"
// Add a value.
session.Values["foo"] = "bar"
// Save.
if err = sessions.Save(req, rsp); err != nil {
t.Fatalf("Error saving session: %v", err)
}
// 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)
// 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)
```

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

@@ -0,0 +1,7 @@
module github.com/boj/redistore
require (
github.com/gomodule/redigo v2.0.0+incompatible
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.1.1
)

View File

@@ -15,7 +15,7 @@ import (
"strings"
"time"
"github.com/garyburd/redigo/redis"
"github.com/gomodule/redigo/redis"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
@@ -252,7 +252,7 @@ func (s *RediStore) New(r *http.Request, name string) (*sessions.Session, error)
// 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 session.Options.MaxAge <= 0 {
if err := s.delete(session); err != nil {
return err
}

View File

@@ -1,404 +0,0 @@
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{})
}

15
vendor/github.com/davecgh/go-spew/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

145
vendor/github.com/davecgh/go-spew/spew/bypass.go generated vendored Normal file
View File

@@ -0,0 +1,145 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew
import (
"reflect"
"unsafe"
)
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = false
// ptrSize is the size of a pointer on the current arch.
ptrSize = unsafe.Sizeof((*byte)(nil))
)
type flag uintptr
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
ro, addr flag
}{{
// From Go 1.4 to 1.5
ro: 1 << 5,
addr: 1 << 7,
}, {
// Up to Go tip.
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data. It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) reflect.Value {
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
return v
}
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with one of the known versions.
for _, f := range okFlags {
if flagRO == f.ro && flagAddr == f.addr {
return
}
}
panic("reflect.Value read-only flag has changed semantics")
}

38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go generated vendored Normal file
View File

@@ -0,0 +1,38 @@
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build js appengine safe disableunsafe !go1.4
package spew
import "reflect"
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = true
)
// unsafeReflectValue typically converts the passed reflect.Value into a one
// that bypasses the typical safety restrictions preventing access to
// unaddressable and unexported data. However, doing this relies on access to
// the unsafe package. This is a stub version which simply returns the passed
// reflect.Value when the unsafe package is not available.
func unsafeReflectValue(v reflect.Value) reflect.Value {
return v
}

341
vendor/github.com/davecgh/go-spew/spew/common.go generated vendored Normal file
View File

@@ -0,0 +1,341 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"reflect"
"sort"
"strconv"
)
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
panicBytes = []byte("(PANIC=")
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
falseBytes = []byte("false")
interfaceBytes = []byte("(interface {})")
commaNewlineBytes = []byte(",\n")
newlineBytes = []byte("\n")
openBraceBytes = []byte("{")
openBraceNewlineBytes = []byte("{\n")
closeBraceBytes = []byte("}")
asteriskBytes = []byte("*")
colonBytes = []byte(":")
colonSpaceBytes = []byte(": ")
openParenBytes = []byte("(")
closeParenBytes = []byte(")")
spaceBytes = []byte(" ")
pointerChainBytes = []byte("->")
nilAngleBytes = []byte("<nil>")
maxNewlineBytes = []byte("<max depth reached>\n")
maxShortBytes = []byte("<max>")
circularBytes = []byte("<already shown>")
circularShortBytes = []byte("<shown>")
invalidAngleBytes = []byte("<invalid>")
openBracketBytes = []byte("[")
closeBracketBytes = []byte("]")
percentBytes = []byte("%")
precisionBytes = []byte(".")
openAngleBytes = []byte("<")
closeAngleBytes = []byte(">")
openMapBytes = []byte("map[")
closeMapBytes = []byte("]")
lenEqualsBytes = []byte("len=")
capEqualsBytes = []byte("cap=")
)
// hexDigits is used to map a decimal value to a hex digit.
var hexDigits = "0123456789abcdef"
// catchPanic handles any panics that might occur during the handleMethods
// calls.
func catchPanic(w io.Writer, v reflect.Value) {
if err := recover(); err != nil {
w.Write(panicBytes)
fmt.Fprintf(w, "%v", err)
w.Write(closeParenBytes)
}
}
// handleMethods attempts to call the Error and String methods on the underlying
// type the passed reflect.Value represents and outputes the result to Writer w.
//
// It handles panics in any called methods by catching and displaying the error
// as the formatted value.
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
// We need an interface to check if the type implements the error or
// Stringer interface. However, the reflect package won't give us an
// interface on certain things like unexported struct fields in order
// to enforce visibility rules. We use unsafe, when it's available,
// to bypass these restrictions since this package does not mutate the
// values.
if !v.CanInterface() {
if UnsafeDisabled {
return false
}
v = unsafeReflectValue(v)
}
// Choose whether or not to do error and Stringer interface lookups against
// the base type or a pointer to the base type depending on settings.
// Technically calling one of these methods with a pointer receiver can
// mutate the value, however, types which choose to satisify an error or
// Stringer interface with a pointer receiver should not be mutating their
// state inside these interface methods.
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
v = unsafeReflectValue(v)
}
if v.CanAddr() {
v = v.Addr()
}
// Is it an error or Stringer?
switch iface := v.Interface().(type) {
case error:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.Error()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.Error()))
return true
case fmt.Stringer:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.String()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.String()))
return true
}
return false
}
// printBool outputs a boolean value as true or false to Writer w.
func printBool(w io.Writer, val bool) {
if val {
w.Write(trueBytes)
} else {
w.Write(falseBytes)
}
}
// printInt outputs a signed integer value to Writer w.
func printInt(w io.Writer, val int64, base int) {
w.Write([]byte(strconv.FormatInt(val, base)))
}
// printUint outputs an unsigned integer value to Writer w.
func printUint(w io.Writer, val uint64, base int) {
w.Write([]byte(strconv.FormatUint(val, base)))
}
// printFloat outputs a floating point value using the specified precision,
// which is expected to be 32 or 64bit, to Writer w.
func printFloat(w io.Writer, val float64, precision int) {
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
}
// printComplex outputs a complex value using the specified float precision
// for the real and imaginary parts to Writer w.
func printComplex(w io.Writer, c complex128, floatPrecision int) {
r := real(c)
w.Write(openParenBytes)
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
i := imag(c)
if i >= 0 {
w.Write(plusBytes)
}
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
w.Write(iBytes)
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
num := uint64(p)
if num == 0 {
w.Write(nilAngleBytes)
return
}
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
buf := make([]byte, 18)
// It's simpler to construct the hex string right to left.
base := uint64(16)
i := len(buf) - 1
for num >= base {
buf[i] = hexDigits[num%base]
num /= base
i--
}
buf[i] = hexDigits[num]
// Add '0x' prefix.
i--
buf[i] = 'x'
i--
buf[i] = '0'
// Strip unused leading bytes.
buf = buf[i:]
w.Write(buf)
}
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
// elements to be sorted.
type valuesSorter struct {
values []reflect.Value
strings []string // either nil or same len and values
cs *ConfigState
}
// newValuesSorter initializes a valuesSorter instance, which holds a set of
// surrogate keys on which the data should be sorted. It uses flags in
// ConfigState to decide if and how to populate those surrogate keys.
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
vs := &valuesSorter{values: values, cs: cs}
if canSortSimply(vs.values[0].Kind()) {
return vs
}
if !cs.DisableMethods {
vs.strings = make([]string, len(values))
for i := range vs.values {
b := bytes.Buffer{}
if !handleMethods(cs, &b, vs.values[i]) {
vs.strings = nil
break
}
vs.strings[i] = b.String()
}
}
if vs.strings == nil && cs.SpewKeys {
vs.strings = make([]string, len(values))
for i := range vs.values {
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
}
}
return vs
}
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
// directly, or whether it should be considered for sorting by surrogate keys
// (if the ConfigState allows it).
func canSortSimply(kind reflect.Kind) bool {
// This switch parallels valueSortLess, except for the default case.
switch kind {
case reflect.Bool:
return true
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Uintptr:
return true
case reflect.Array:
return true
}
return false
}
// Len returns the number of values in the slice. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Len() int {
return len(s.values)
}
// Swap swaps the values at the passed indices. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Swap(i, j int) {
s.values[i], s.values[j] = s.values[j], s.values[i]
if s.strings != nil {
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
}
}
// valueSortLess returns whether the first value should sort before the second
// value. It is used by valueSorter.Less as part of the sort.Interface
// implementation.
func valueSortLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.Bool:
return !a.Bool() && b.Bool()
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return a.Int() < b.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return a.Uint() < b.Uint()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.String:
return a.String() < b.String()
case reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Array:
// Compare the contents of both arrays.
l := a.Len()
for i := 0; i < l; i++ {
av := a.Index(i)
bv := b.Index(i)
if av.Interface() == bv.Interface() {
continue
}
return valueSortLess(av, bv)
}
}
return a.String() < b.String()
}
// Less returns whether the value at index i should sort before the
// value at index j. It is part of the sort.Interface implementation.
func (s *valuesSorter) Less(i, j int) bool {
if s.strings == nil {
return valueSortLess(s.values[i], s.values[j])
}
return s.strings[i] < s.strings[j]
}
// sortValues is a sort function that handles both native types and any type that
// can be converted to error or Stringer. Other inputs are sorted according to
// their Value.String() value to ensure display stability.
func sortValues(values []reflect.Value, cs *ConfigState) {
if len(values) == 0 {
return
}
sort.Sort(newValuesSorter(values, cs))
}

306
vendor/github.com/davecgh/go-spew/spew/config.go generated vendored Normal file
View File

@@ -0,0 +1,306 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"os"
)
// ConfigState houses the configuration options used by spew to format and
// display values. There is a global instance, Config, that is used to control
// all top-level Formatter and Dump functionality. Each ConfigState instance
// provides methods equivalent to the top-level functions.
//
// The zero value for ConfigState provides no indentation. You would typically
// want to set it to a space or a tab.
//
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
// with default settings. See the documentation of NewDefaultConfig for default
// values.
type ConfigState struct {
// Indent specifies the string to use for each indentation level. The
// global config instance that all top-level functions use set this to a
// single space by default. If you would like more indentation, you might
// set this to a tab with "\t" or perhaps two spaces with " ".
Indent string
// MaxDepth controls the maximum number of levels to descend into nested
// data structures. The default, 0, means there is no limit.
//
// NOTE: Circular data structures are properly detected, so it is not
// necessary to set this value unless you specifically want to limit deeply
// nested data structures.
MaxDepth int
// DisableMethods specifies whether or not error and Stringer interfaces are
// invoked for types that implement them.
DisableMethods bool
// DisablePointerMethods specifies whether or not to check for and invoke
// error and Stringer interfaces on types which only accept a pointer
// receiver when the current type is not a pointer.
//
// NOTE: This might be an unsafe action since calling one of these methods
// with a pointer receiver could technically mutate the value, however,
// in practice, types which choose to satisify an error or Stringer
// interface with a pointer receiver should not be mutating their state
// inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer
// interface and return immediately instead of continuing to recurse into
// the internals of the data type.
//
// NOTE: This flag does not have any effect if method invocation is disabled
// via the DisableMethods or DisablePointerMethods options.
ContinueOnMethod bool
// SortKeys specifies map keys should be sorted before being printed. Use
// this to have a more deterministic, diffable output. Note that only
// native types (bool, int, uint, floats, uintptr and string) and types
// that support the error or Stringer interfaces (if methods are
// enabled) are supported, with other types sorted according to the
// reflect.Value.String() output which guarantees display stability.
SortKeys bool
// SpewKeys specifies that, as a last resort attempt, map keys should
// be spewed to strings and sorted by those strings. This is only
// considered if SortKeys is true.
SpewKeys bool
}
// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of spew.Config.
var Config = ConfigState{Indent: " "}
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the formatted string as a value that satisfies error. See NewFormatter
// for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, c.convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, c.convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, c.convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a Formatter interface returned by c.NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
return fmt.Print(c.convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, c.convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
return fmt.Println(c.convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprint(a ...interface{}) string {
return fmt.Sprint(c.convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, c.convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a Formatter interface returned by c.NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.convertArgs(a)...)
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
c.Printf, c.Println, or c.Printf.
*/
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(c, v)
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
fdump(c, w, a...)
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func (c *ConfigState) Dump(a ...interface{}) {
fdump(c, os.Stdout, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func (c *ConfigState) Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(c, &buf, a...)
return buf.String()
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a spew Formatter interface using
// the ConfigState associated with s.
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = newFormatter(c, arg)
}
return formatters
}
// NewDefaultConfig returns a ConfigState with the following default settings.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}

211
vendor/github.com/davecgh/go-spew/spew/doc.go generated vendored Normal file
View File

@@ -0,0 +1,211 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package spew implements a deep pretty printer for Go data structures to aid in
debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
* Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...)
Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
via the spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
* Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
* SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
* SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
Dump Usage
Simply call spew.Dump with a list of variables you want to dump:
spew.Dump(myVar1, myVar2, ...)
You may also call spew.Fdump if you would prefer to output to an arbitrary
io.Writer. For example, to dump to standard error:
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...)
Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr) <nil>
}),
ExportedField: (map[interface {}]interface {}) (len=1) {
(string) (len=3) "one": (bool) true
}
}
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
so that it integrates cleanly with standard fmt package printing functions. The
formatter is useful for inline printing of smaller data types similar to the
standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
functions have syntax you are most likely already familiar with:
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Println(myVar, myVar2)
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
See the Index for the full list convenience functions.
Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
See the Printf example for details on the setup of variables being shown
here.
Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information
inline with the output. Since spew is intended to provide deep pretty printing
capabilities on structures, it intentionally does not return any errors.
*/
package spew

509
vendor/github.com/davecgh/go-spew/spew/dump.go generated vendored Normal file
View File

@@ -0,0 +1,509 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"os"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
// uint8Type is a reflect.Type representing a uint8. It is used to
// convert cgo types to uint8 slices for hexdumping.
uint8Type = reflect.TypeOf(uint8(0))
// cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
)
// dumpState contains information about the state of a dump operation.
type dumpState struct {
w io.Writer
depth int
pointers map[uintptr]int
ignoreNextType bool
ignoreNextIndent bool
cs *ConfigState
}
// indent performs indentation according to the depth level and cs.Indent
// option.
func (d *dumpState) indent() {
if d.ignoreNextIndent {
d.ignoreNextIndent = false
return
}
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
}
// unpackValue returns values inside of non-nil interfaces when possible.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
}
return v
}
// dumpPtr handles formatting of pointers by indirecting them as necessary.
func (d *dumpState) dumpPtr(v reflect.Value) {
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range d.pointers {
if depth >= d.depth {
delete(d.pointers, k)
}
}
// Keep list of all dereferenced pointers to show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by dereferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
cycleFound = true
indirects--
break
}
d.pointers[addr] = d.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type information.
d.w.Write(openParenBytes)
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
d.w.Write([]byte(ve.Type().String()))
d.w.Write(closeParenBytes)
// Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
d.w.Write(pointerChainBytes)
}
printHexPtr(d.w, addr)
}
d.w.Write(closeParenBytes)
}
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound:
d.w.Write(circularBytes)
default:
d.ignoreNextType = true
d.dump(ve)
}
d.w.Write(closeParenBytes)
}
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
// reflection) arrays and slices are dumped in hexdump -C fashion.
func (d *dumpState) dumpSlice(v reflect.Value) {
// Determine whether this type should be hex dumped or not. Also,
// for types which should be hexdumped, try to use the underlying data
// first, then fall back to trying to convert them to a uint8 slice.
var buf []uint8
doConvert := false
doHexDump := false
numEntries := v.Len()
if numEntries > 0 {
vt := v.Index(0).Type()
vts := vt.String()
switch {
// C types that need to be converted.
case cCharRE.MatchString(vts):
fallthrough
case cUnsignedCharRE.MatchString(vts):
fallthrough
case cUint8tCharRE.MatchString(vts):
doConvert = true
// Try to use existing uint8 slices and fall back to converting
// and copying if that fails.
case vt.Kind() == reflect.Uint8:
// We need an addressable interface to convert the type
// to a byte slice. However, the reflect package won't
// give us an interface on certain things like
// unexported struct fields in order to enforce
// visibility rules. We use unsafe, when available, to
// bypass these restrictions since this package does not
// mutate the values.
vs := v
if !vs.CanInterface() || !vs.CanAddr() {
vs = unsafeReflectValue(vs)
}
if !UnsafeDisabled {
vs = vs.Slice(0, numEntries)
// Use the existing uint8 slice if it can be
// type asserted.
iface := vs.Interface()
if slice, ok := iface.([]uint8); ok {
buf = slice
doHexDump = true
break
}
}
// The underlying data needs to be converted if it can't
// be type asserted to a uint8 slice.
doConvert = true
}
// Copy and convert the underlying type if needed.
if doConvert && vt.ConvertibleTo(uint8Type) {
// Convert and copy each element into a uint8 byte
// slice.
buf = make([]uint8, numEntries)
for i := 0; i < numEntries; i++ {
vv := v.Index(i)
buf[i] = uint8(vv.Convert(uint8Type).Uint())
}
doHexDump = true
}
}
// Hexdump the entire slice as needed.
if doHexDump {
indent := strings.Repeat(d.cs.Indent, d.depth)
str := indent + hex.Dump(buf)
str = strings.Replace(str, "\n", "\n"+indent, -1)
str = strings.TrimRight(str, d.cs.Indent)
d.w.Write([]byte(str))
return
}
// Recursively call dump for each item.
for i := 0; i < numEntries; i++ {
d.dump(d.unpackValue(v.Index(i)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
// dump is the main workhorse for dumping a value. It uses the passed reflect
// value to figure out what kind of object we are dealing with and formats it
// appropriately. It is a recursive function, however circular data structures
// are detected and handled properly.
func (d *dumpState) dump(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
d.w.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
d.indent()
d.dumpPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !d.ignoreNextType {
d.indent()
d.w.Write(openParenBytes)
d.w.Write([]byte(v.Type().String()))
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
d.ignoreNextType = false
// Display length and capacity if the built-in len and cap functions
// work with the value's kind and the len/cap itself is non-zero.
valueLen, valueCap := 0, 0
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan:
valueLen, valueCap = v.Len(), v.Cap()
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}
d.w.Write(capEqualsBytes)
printInt(d.w, int64(valueCap), 10)
}
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
// Call Stringer/error interfaces if they exist and the handle methods flag
// is enabled
if !d.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(d.cs, d.w, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(d.w, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(d.w, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(d.w, v.Uint(), 10)
case reflect.Float32:
printFloat(d.w, v.Float(), 32)
case reflect.Float64:
printFloat(d.w, v.Float(), 64)
case reflect.Complex64:
printComplex(d.w, v.Complex(), 32)
case reflect.Complex128:
printComplex(d.w, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
d.dumpSlice(v)
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.String:
d.w.Write([]byte(strconv.Quote(v.String())))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
d.w.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
numEntries := v.Len()
keys := v.MapKeys()
if d.cs.SortKeys {
sortValues(keys, d.cs)
}
for i, key := range keys {
d.dump(d.unpackValue(key))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.MapIndex(key)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Struct:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
vt := v.Type()
numFields := v.NumField()
for i := 0; i < numFields; i++ {
d.indent()
vtf := vt.Field(i)
d.w.Write([]byte(vtf.Name))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.Field(i)))
if i < (numFields - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(d.w, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(d.w, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it in case any new
// types are added.
default:
if v.CanInterface() {
fmt.Fprintf(d.w, "%v", v.Interface())
} else {
fmt.Fprintf(d.w, "%v", v.String())
}
}
}
// fdump is a helper function to consolidate the logic from the various public
// methods which take varying writers and config states.
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
for _, arg := range a {
if arg == nil {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
continue
}
d := dumpState{w: w, cs: cs}
d.pointers = make(map[uintptr]int)
d.dump(reflect.ValueOf(arg))
d.w.Write(newlineBytes)
}
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func Fdump(w io.Writer, a ...interface{}) {
fdump(&Config, w, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(&Config, &buf, a...)
return buf.String()
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global,
spew.Config. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func Dump(a ...interface{}) {
fdump(&Config, os.Stdout, a...)
}

419
vendor/github.com/davecgh/go-spew/spew/format.go generated vendored Normal file
View File

@@ -0,0 +1,419 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
)
// supportedFlags is a list of all the character flags supported by fmt package.
const supportedFlags = "0-+# "
// formatState implements the fmt.Formatter interface and contains information
// about the state of a formatting operation. The NewFormatter function can
// be used to get a new Formatter which can be used directly as arguments
// in standard fmt package printing calls.
type formatState struct {
value interface{}
fs fmt.State
depth int
pointers map[uintptr]int
ignoreNextType bool
cs *ConfigState
}
// buildDefaultFormat recreates the original format string without precision
// and width information to pass in to fmt.Sprintf in the case of an
// unrecognized type. Unless new types are added to the language, this
// function won't ever be called.
func (f *formatState) buildDefaultFormat() (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
buf.WriteRune('v')
format = buf.String()
return format
}
// constructOrigFormat recreates the original format string including precision
// and width information to pass along to the standard fmt package. This allows
// automatic deferral of all format strings this package doesn't support.
func (f *formatState) constructOrigFormat(verb rune) (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
if width, ok := f.fs.Width(); ok {
buf.WriteString(strconv.Itoa(width))
}
if precision, ok := f.fs.Precision(); ok {
buf.Write(precisionBytes)
buf.WriteString(strconv.Itoa(precision))
}
buf.WriteRune(verb)
format = buf.String()
return format
}
// unpackValue returns values inside of non-nil interfaces when possible and
// ensures that types for values which have been unpacked from an interface
// are displayed when the show types flag is also set.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface {
f.ignoreNextType = false
if !v.IsNil() {
v = v.Elem()
}
}
return v
}
// formatPtr handles formatting of pointers by indirecting them as necessary.
func (f *formatState) formatPtr(v reflect.Value) {
// Display nil if top level pointer is nil.
showTypes := f.fs.Flag('#')
if v.IsNil() && (!showTypes || f.ignoreNextType) {
f.fs.Write(nilAngleBytes)
return
}
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range f.pointers {
if depth >= f.depth {
delete(f.pointers, k)
}
}
// Keep list of all dereferenced pointers to possibly show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by derferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
cycleFound = true
indirects--
break
}
f.pointers[addr] = f.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type or indirection level depending on flags.
if showTypes && !f.ignoreNextType {
f.fs.Write(openParenBytes)
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
f.fs.Write([]byte(ve.Type().String()))
f.fs.Write(closeParenBytes)
} else {
if nilFound || cycleFound {
indirects += strings.Count(ve.Type().String(), "*")
}
f.fs.Write(openAngleBytes)
f.fs.Write([]byte(strings.Repeat("*", indirects)))
f.fs.Write(closeAngleBytes)
}
// Display pointer information depending on flags.
if f.fs.Flag('+') && (len(pointerChain) > 0) {
f.fs.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
f.fs.Write(pointerChainBytes)
}
printHexPtr(f.fs, addr)
}
f.fs.Write(closeParenBytes)
}
// Display dereferenced value.
switch {
case nilFound:
f.fs.Write(nilAngleBytes)
case cycleFound:
f.fs.Write(circularShortBytes)
default:
f.ignoreNextType = true
f.format(ve)
}
}
// format is the main workhorse for providing the Formatter interface. It
// uses the passed reflect value to figure out what kind of object we are
// dealing with and formats it appropriately. It is a recursive function,
// however circular data structures are detected and handled properly.
func (f *formatState) format(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
f.fs.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
f.formatPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !f.ignoreNextType && f.fs.Flag('#') {
f.fs.Write(openParenBytes)
f.fs.Write([]byte(v.Type().String()))
f.fs.Write(closeParenBytes)
}
f.ignoreNextType = false
// Call Stringer/error interfaces if they exist and the handle methods
// flag is enabled.
if !f.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(f.cs, f.fs, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(f.fs, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(f.fs, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(f.fs, v.Uint(), 10)
case reflect.Float32:
printFloat(f.fs, v.Float(), 32)
case reflect.Float64:
printFloat(f.fs, v.Float(), 64)
case reflect.Complex64:
printComplex(f.fs, v.Complex(), 32)
case reflect.Complex128:
printComplex(f.fs, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
f.fs.Write(openBracketBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
numEntries := v.Len()
for i := 0; i < numEntries; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(v.Index(i)))
}
}
f.depth--
f.fs.Write(closeBracketBytes)
case reflect.String:
f.fs.Write([]byte(v.String()))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
f.fs.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
f.fs.Write(openMapBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
keys := v.MapKeys()
if f.cs.SortKeys {
sortValues(keys, f.cs)
}
for i, key := range keys {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(key))
f.fs.Write(colonBytes)
f.ignoreNextType = true
f.format(f.unpackValue(v.MapIndex(key)))
}
}
f.depth--
f.fs.Write(closeMapBytes)
case reflect.Struct:
numFields := v.NumField()
f.fs.Write(openBraceBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
vt := v.Type()
for i := 0; i < numFields; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
vtf := vt.Field(i)
if f.fs.Flag('+') || f.fs.Flag('#') {
f.fs.Write([]byte(vtf.Name))
f.fs.Write(colonBytes)
}
f.format(f.unpackValue(v.Field(i)))
}
}
f.depth--
f.fs.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(f.fs, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(f.fs, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it if any get added.
default:
format := f.buildDefaultFormat()
if v.CanInterface() {
fmt.Fprintf(f.fs, format, v.Interface())
} else {
fmt.Fprintf(f.fs, format, v.String())
}
}
}
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
// details.
func (f *formatState) Format(fs fmt.State, verb rune) {
f.fs = fs
// Use standard formatting for verbs that are not v.
if verb != 'v' {
format := f.constructOrigFormat(verb)
fmt.Fprintf(fs, format, f.value)
return
}
if f.value == nil {
if fs.Flag('#') {
fs.Write(interfaceBytes)
}
fs.Write(nilAngleBytes)
return
}
f.format(reflect.ValueOf(f.value))
}
// newFormatter is a helper function to consolidate the logic from the various
// public methods which take varying config states.
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
fs := &formatState{value: v, cs: cs}
fs.pointers = make(map[uintptr]int)
return fs
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
Printf, Println, or Fprintf.
*/
func NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(&Config, v)
}

148
vendor/github.com/davecgh/go-spew/spew/spew.go generated vendored Normal file
View File

@@ -0,0 +1,148 @@
/*
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"fmt"
"io"
)
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the formatted string as a value that satisfies error. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a default Formatter interface returned by NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
func Print(a ...interface{}) (n int, err error) {
return fmt.Print(convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
func Println(a ...interface{}) (n int, err error) {
return fmt.Println(convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprint(a ...interface{}) string {
return fmt.Sprint(convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintln(a ...interface{}) string {
return fmt.Sprintln(convertArgs(a)...)
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a default spew Formatter interface.
func convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = NewFormatter(arg)
}
return formatters
}

1
vendor/github.com/deepzz0/logd/.gitignore generated vendored Normal file
View File

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

149
vendor/github.com/deepzz0/logd/README.md generated vendored Normal file
View File

@@ -0,0 +1,149 @@
# Logd
高性能日志系统每日24时自动打包前一天数据自动删除过期数据30天自动邮件告警。
#### Bechmark
```
BenchmarkLogFileChan-8 500000 2265 ns/op
BenchmarkLogFile-8 300000 4393 ns/op
BenchmarkStandardFile-8 1000000 2216 ns/op
BenchmarkLogFileChanMillion-8 1000000 2319 ns/op
BenchmarkLogFileMillion-8 1000000 4397 ns/op
BenchmarkStandardFileMillion-8 1000000 2232 ns/op
```
#### Print Level
```
Ldebug = 1 << iota //
Linfo //
Lwarn //
Lerror //
Lfatal //
logd.Printf()
logd.Print()
logd.Debugf()
logd.Debug()
logd.Infof()
logd.Info()
logd.Warnf()
logd.Warn()
logd.Errorf()
logd.Error()
logd.Fatalf()
logd.Fatal()
```
5种日志等级12种打印方法完全理解你记录日志的需求。
```
Ldebug < Linfo < Lwarn < Lerror < Lfatal
```
通过`SetLevel()`方法设置logd.SetLevel(Lwarn)系统只会打印大于等于Lwarn的日志。
> 注意logd.Printf() 和 logd.Print() 不论等级为什么都会输出,请悉知。
#### Print Option
系统提供多种格式输出选项:
```
Lasync // 异步输出日志
Ldate // like 2006/01/02
Ltime // like 15:04:05
Lmicroseconds // like 15:04:05.123123
Llongfile // like /a/b/c/d.go:23
Lshortfile // like d.go:23
LUTC // 时间utc输出
Ldaily // 按天归档
默认提供:
LstdFlags // 2006/01/02 15:04:05.123123, /a/b/c/d.go:23
```
#### 使用方法
1、默认方式
默认方式使用以下配置选项:
```
// 2006/01/02 15:04:05.123123, /a/b/c/d.go:23
Lall = Ldebug | Linfo | Lwarn | Lerror | Lfatal
LstdFlags = Lall | Ldate | Lmicroseconds | Lshortfile
```
这里会打印所有等级日志,打印日志格式为`2006/01/02 15:04:05.123123, /a/b/c/d.go:23`
可以通过`logd.SetLevel(level)`方式调整日志等级。`logd.SetOutput(writer)`输出日志到别的地方。例子:
```
package main
import (
"fmt"
"os"
"github.com/deepzz0/logd"
)
func main() {
logd.Printf("Printf: foo\n")
logd.Print("Print: foo")
logd.Debugf("Debugf: foo\n")
logd.Debug("Debug: foo")
logd.Errorf("Errorf: foo\n")
logd.Error("Error: foo")
// 改变输出等级
logd.SetLevel(logd.Lerror)
// 不论等级如何都会输出
fmt.Println("----- 改变日志等级为Lerror -----")
logd.Printf("Printf: foo\n")
logd.Print("Print: foo")
logd.Debugf("Debugf: foo\n")
logd.Debug("Debug: foo")
logd.Errorf("Errorf: foo\n")
logd.Error("Error: foo")
fmt.Println("----- 日志等级为Lerror并写入到test.log中 -----")
f, err := os.Create("./test.log")
if err != nil {
panic(err)
}
defer f.Close()
logd.SetOutput(f)
logd.Printf("Printf: foo\n")
logd.Print("Print: foo")
logd.Debugf("Debugf: foo\n")
logd.Debug("Debug: foo")
logd.Errorf("Errorf: foo\n")
logd.Error("Error: foo")
}
```
输出如下:
![logd_print](http://7xokm2.com1.z0.glb.clouddn.com/img/logd_print.png)
1、自定义配置
推荐需要将日志保存下来的用户使用。
```
Flags := Lwarn | Lerror | Lfatal | Ldate | Ltime | Lshortfile | Ldaily | Lasync
f, _ := os.OpenFile("testdata/onlyfile.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
log := New(LogOption{
Out: f,
Flag: Flags,
LogDir: "testdata",
ChannelLen: 1000,
})
```
* 输出日志等级Lwarn | Lerror | Lfatal
* 输出日志格式Ldate | Ltime | Lshortfile
* 按日归档Ldaily
* 异步输出Lasync
它会将每天的日志,采用 gzip 压缩,并保留 30 天以内的日志。自动删除 30 以前的日志。

499
vendor/github.com/deepzz0/logd/logd.go generated vendored Normal file
View File

@@ -0,0 +1,499 @@
package logd
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)
const (
Ldebug = 1 << iota //
Linfo //
Lwarn //
Lerror //
Lfatal //
Ldate // 如2006/01/02
Ltime // 如15:04:05
Lmicroseconds // 如15:04:05.123123
Llongfile // 如:/a/b/c/d.go:23
Lshortfile // 如d.go:23
LUTC // 时间utc输出
LAsync // 异步输出日志
LDaily // 按日归档保留30天
//
// 建议标准格式为: LVEVL | FORMAT | ROTATE | ASYNC
//
Lall = Ldebug | Linfo | Lwarn | Lerror | Lfatal
// 2006/01/02 15:04:05.123123, d.go:23
LstdFlags = Lall | Ldate | Lmicroseconds | Lshortfile
// 2006/01/02 15:04:05, d.go:23
LwarnFlags = Lwarn | Lerror | Lfatal | Ldate | Ltime | Lshortfile | LDaily | LAsync
)
// 等级显示字符串
var levelMaps = map[int]string{
Ldebug: "DEBUG",
Linfo: "INFO",
Lwarn: "WARN",
Lerror: "ERROR",
Lfatal: "FATAL",
}
// 日志结构体
type Logger struct {
mu sync.Mutex
obj string // 打印日志对象
out io.Writer // 输出
in chan []byte // channel
dir string // 输出目录
flag int // 标志
mails Emailer // 告警邮件
}
// 日志配置项
type LogOption struct {
Out io.Writer // 输出writer
LogDir string // 日志输出目录,为空不输出到文件
ChannelLen int // channel
Flag int // 标志位
Mails Emailer // 告警邮件
}
func osSep() string {
var sep string
if os.IsPathSeparator('\\') {
sep = "\\"
} else {
sep = "/"
}
return sep
}
// 新建日志打印器
func New(option LogOption) *Logger {
wd, _ := os.Getwd()
index := strings.LastIndex(wd, osSep())
logger := &Logger{
obj: wd[index+1:],
out: option.Out,
in: make(chan []byte, option.ChannelLen),
dir: option.LogDir,
flag: option.Flag,
mails: option.Mails,
}
if logger.flag|LAsync != 0 {
go logger.receive()
}
return logger
}
func (l *Logger) receive() {
today := time.Now()
var file *os.File
var err error
for data := range l.in {
if l.dir != "" && (file == nil || today.Day() != time.Now().Day()) {
l.mu.Lock()
today = time.Now()
file, err = os.OpenFile(fmt.Sprintf("%s/%s_%s.log", l.dir, l.obj, today.Format("2006-01-02")), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
if err != nil {
panic(err)
}
l.mu.Unlock()
if l.flag&LDaily != 0 {
go l.rotate(today)
}
}
if file != nil {
file.Write(data)
}
if l.out != nil {
l.out.Write(data)
}
}
}
// 压缩
func (l *Logger) rotate(t time.Time) {
filepath.Walk(l.dir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if int(t.Sub(f.ModTime()).Hours()) > 24 {
if strings.HasSuffix(f.Name(), ".log") {
cmd := exec.Command("gzip", path)
err = cmd.Run()
if err != nil {
return err
}
}
}
if int(t.Sub(f.ModTime()).Hours()) > 24*30 {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
}
// 日志基本格式: date, time(hour:minute:second:microsecond), level, module, shortfile:line, <content>
func (l *Logger) Output(lvl int, calldepth int, content string) error {
_, file, line, ok := runtime.Caller(calldepth)
if !ok {
return nil
}
var buf []byte
l.formatHeader(&buf, lvl, time.Now(), file, line)
buf = append(buf, content...)
if l.mails != nil && lvl >= Lwarn {
go l.mails.SendMail(l.obj, buf)
}
// 异步输出
if l.flag&LAsync != 0 {
l.in <- buf
} else {
l.mu.Lock()
defer l.mu.Unlock()
l.out.Write(buf)
}
return nil
}
// 整理日志header
func (l *Logger) formatHeader(buf *[]byte, lvl int, t time.Time, file string, line int) {
if l.flag&LUTC != 0 {
t = t.UTC()
}
if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {
if l.flag&Ldate != 0 {
year, month, day := t.Date()
itoa(buf, year, 4)
*buf = append(*buf, '/')
itoa(buf, int(month), 2)
*buf = append(*buf, '/')
itoa(buf, day, 2)
*buf = append(*buf, ' ')
}
if l.flag&(Ltime|Lmicroseconds) != 0 {
hour, min, sec := t.Clock()
itoa(buf, hour, 2)
*buf = append(*buf, ':')
itoa(buf, min, 2)
*buf = append(*buf, ':')
itoa(buf, sec, 2)
if l.flag&Lmicroseconds != 0 {
*buf = append(*buf, '.')
itoa(buf, t.Nanosecond()/1e3, 6)
}
*buf = append(*buf, ' ')
}
}
*buf = append(*buf, getColorLevel(levelMaps[lvl])...)
*buf = append(*buf, ' ')
if l.flag&(Lshortfile|Llongfile) != 0 {
if l.flag&Lshortfile != 0 {
short := file
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
file = short
}
*buf = append(*buf, file...)
*buf = append(*buf, ':')
itoa(buf, line, -1)
*buf = append(*buf, ":"...)
}
}
// 等待flush channel
func (l *Logger) WaitFlush() {
for {
if len(l.in) > 0 {
time.Sleep(time.Nanosecond * 50)
} else {
break
}
}
}
// print
func (l *Logger) Printf(format string, v ...interface{}) {
l.Output(Linfo, 2, fmt.Sprintf(format, v...))
}
func (l *Logger) Print(v ...interface{}) {
l.Output(Linfo, 2, fmt.Sprintf(smartFormat(v...), v...))
}
// debug
func (l *Logger) Debugf(format string, v ...interface{}) {
if Ldebug&l.flag != 0 {
l.Output(Ldebug, 2, fmt.Sprintf(format, v...))
}
}
func (l *Logger) Debug(v ...interface{}) {
if Ldebug&l.flag != 0 {
l.Output(Ldebug, 2, fmt.Sprintf(smartFormat(v...), v...))
}
}
// info
func (l *Logger) Infof(format string, v ...interface{}) {
if Linfo&l.flag != 0 {
l.Output(Linfo, 2, fmt.Sprintf(format, v...))
}
}
func (l *Logger) Info(v ...interface{}) {
if Linfo&l.flag != 0 {
l.Output(Linfo, 2, fmt.Sprintf(smartFormat(v...), v...))
}
}
// warn
func (l *Logger) Warnf(format string, v ...interface{}) {
if Lwarn&l.flag != 0 {
l.Output(Lwarn, 2, fmt.Sprintf(format, v...))
}
}
func (l *Logger) Warn(v ...interface{}) {
if Lwarn&l.flag != 0 {
l.Output(Lwarn, 2, fmt.Sprintf(smartFormat(v...), v...))
}
}
// error
func (l *Logger) Errorf(format string, v ...interface{}) {
if Lerror&l.flag != 0 {
l.Output(Lerror, 2, fmt.Sprintf(format, v...)+CallerStack())
}
}
func (l *Logger) Error(v ...interface{}) {
if Lerror&l.flag != 0 {
l.Output(Lerror, 2, fmt.Sprintf(smartFormat(v...), v...)+CallerStack())
}
}
// fatal
func (l *Logger) Fatalf(format string, v ...interface{}) {
l.Output(Lfatal, 2, fmt.Sprintf(format, v...))
os.Exit(1)
}
func (l *Logger) Fatal(v ...interface{}) {
l.Output(Lfatal, 2, fmt.Sprintf(smartFormat(v...), v...))
os.Exit(1)
}
func (l *Logger) Breakpoint() {
l.Output(Ldebug, 3, fmt.Sprintln("breakpoint"))
}
// 设置日志目录
func (l *Logger) SetLogDir(dir string) {
l.mu.Lock()
defer l.mu.Unlock()
l.dir = dir
}
// 设置日志对象名称
func (l *Logger) SetObj(obj string) {
l.mu.Lock()
defer l.mu.Unlock()
l.obj = obj
}
// 改变日志输出writer
func (l *Logger) SetOutput(out io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.out = out
}
// 设置日志等级
func (l *Logger) SetLevel(lvl int) {
l.mu.Lock()
defer l.mu.Unlock()
var i uint
for i = 0; i < uint(len(levelMaps)); i++ {
if lvl&1 != 0 {
break
}
lvl >>= 1
}
l.flag = (l.flag >> i) | (Lall >> i)
l.flag <<= i
}
// standard wrapper
var Std = New(LogOption{Out: os.Stdout, Flag: LstdFlags})
func Printf(format string, v ...interface{}) {
Std.Output(Linfo, 2, fmt.Sprintf(format, v...))
}
func Print(v ...interface{}) {
Std.Output(Linfo, 2, fmt.Sprintf(smartFormat(v...), v...))
}
func Debugf(format string, v ...interface{}) {
if Ldebug&Std.flag != 0 {
Std.Output(Ldebug, 2, fmt.Sprintf(format, v...))
}
}
func Debug(v ...interface{}) {
if Ldebug&Std.flag != 0 {
Std.Output(Ldebug, 2, fmt.Sprintf(smartFormat(v...), v...))
}
}
func Infof(format string, v ...interface{}) {
if Linfo&Std.flag != 0 {
Std.Output(Linfo, 2, fmt.Sprintf(format, v...))
}
}
func Info(v ...interface{}) {
if Linfo&Std.flag != 0 {
Std.Output(Linfo, 2, fmt.Sprintf(smartFormat(v...), v...))
}
}
func Warnf(format string, v ...interface{}) {
if Lwarn&Std.flag != 0 {
Std.Output(Lwarn, 2, fmt.Sprintf(format, v...))
}
}
func Warn(v ...interface{}) {
if Lwarn&Std.flag != 0 {
Std.Output(Lwarn, 2, fmt.Sprintf(smartFormat(v...), v...))
}
}
func Errorf(format string, v ...interface{}) {
if Lerror&Std.flag != 0 {
Std.Output(Lerror, 2, fmt.Sprintf(format, v...)+CallerStack())
}
}
func Error(v ...interface{}) {
if Lerror&Std.flag != 0 {
Std.Output(Lerror, 2, fmt.Sprintf(smartFormat(v...), v...)+CallerStack())
}
}
func Fatalf(format string, v ...interface{}) {
Std.Output(Lfatal, 2, fmt.Sprintf(format, v...)+CallerStack())
os.Exit(1)
}
func Fatal(v ...interface{}) {
Std.Output(Lfatal, 2, fmt.Sprintf(smartFormat(v...), v...)+CallerStack())
os.Exit(1)
}
func Breakpoint() {
Std.Breakpoint()
}
func SetLevel(lvl int) {
Std.SetLevel(lvl)
}
func SetOutput(w io.Writer) {
Std.SetOutput(w)
}
func SetObj(obj string) {
Std.SetObj(obj)
}
///////////////////////////////////////////////////////////////////////////////////////////
func smartFormat(v ...interface{}) string {
format := ""
for i := 0; i < len(v); i++ {
format += " %v"
}
format += "\n"
return format
}
// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
// Assemble decimal in reverse order.
var b [20]byte
bp := len(b) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
b[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
b[bp] = byte('0' + i)
*buf = append(*buf, b[bp:]...)
}
const (
Gray = uint8(iota + 90)
Red
Green
Yellow
Blue
Magenta
)
// getColorLevel returns colored level string by given level.
func getColorLevel(level string) string {
level = strings.ToUpper(level)
switch level {
case "DEBUG":
return fmt.Sprintf("\033[%dm[%5s]\033[0m", Green, level)
case "INFO":
return fmt.Sprintf("\033[%dm[%5s]\033[0m", Blue, level)
case "WARN":
return fmt.Sprintf("\033[%dm[%5s]\033[0m", Magenta, level)
case "ERROR":
return fmt.Sprintf("\033[%dm[%5s]\033[0m", Yellow, level)
case "FATAL":
return fmt.Sprintf("\033[%dm[%5s]\033[0m", Red, level)
default:
return level
}
}
func CallerStack() string {
var caller_str string
for skip := 2; ; skip++ {
// 获取调用者的信息
pc, file, line, ok := runtime.Caller(skip)
if !ok {
break
}
func_name := runtime.FuncForPC(pc).Name()
caller_str += "Func : " + func_name + "\nFile:" + file + ":" + fmt.Sprint(line) + "\n"
}
return caller_str
}

80
vendor/github.com/deepzz0/logd/mail.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
package logd
import (
"crypto/tls"
"fmt"
"net/smtp"
"strings"
)
type Emailer interface {
SendMail(fromname string, msg []byte) error
}
type Smtp struct {
From string // 发件箱number@qq.com
Key string // 发件密钥peerdmnoqirqbiaa
Host string // 主机地址smtp.example.com
Port string // 主机端口465
To []string // 发送给object@163.com
Subject string // 标题:警告邮件[goblog]
}
func (s *Smtp) SendMail(fromname string, msg []byte) error {
// 新建连接
conn, err := tls.Dial("tcp", s.Host+":"+s.Port, nil)
if err != nil {
return err
}
// 新建客户端
client, err := smtp.NewClient(conn, s.Host)
if err != nil {
return err
}
// 获取授权
auth := smtp.PlainAuth("", s.From, s.Key, s.Host)
if err = client.Auth(auth); err != nil {
return err
}
// 向服务器发送MAIL命令
if err = client.Mail(s.From); err != nil {
return err
}
// 准备数据
str := fmt.Sprint(
"To:", strings.Join(s.To, ","),
"\r\nFrom:", fmt.Sprintf("%s<%s>", fromname, s.From),
"\r\nSubject:", s.Subject,
"\r\n", "Content-Type:text/plain;charset=UTF-8",
"\r\n\r\n",
)
data := make([]byte, len(str)+len(msg))
copy(data, []byte(str))
copy(data[len(str):], msg)
// RCPT
for _, d := range s.To {
if err := client.Rcpt(d); err != nil {
return err
}
}
// 获取WriteCloser
wc, err := client.Data()
if err != nil {
return err
}
// 写入数据
_, err = wc.Write(data)
if err != nil {
return err
}
wc.Close()
return client.Quit()
}

View File

@@ -1,5 +0,0 @@
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

@@ -1 +0,0 @@
Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis

View File

@@ -1,18 +0,0 @@
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 ./...

View File

@@ -1,50 +0,0 @@
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

@@ -1,27 +0,0 @@
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

@@ -1,68 +0,0 @@
// 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
}

View File

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

View File

@@ -1,416 +0,0 @@
// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package redis
import (
"bytes"
"container/list"
"crypto/rand"
"crypto/sha1"
"errors"
"io"
"strconv"
"sync"
"time"
"github.com/garyburd/redigo/internal"
)
var nowFunc = time.Now // for testing
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
// Receive, Flush, Err) when the maximum number of database connections in the
// pool has been reached.
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
var (
errPoolClosed = errors.New("redigo: connection pool closed")
errConnClosed = errors.New("redigo: connection closed")
)
// Pool maintains a pool of connections. The application calls the Get method
// to get a connection from the pool and the connection's Close method to
// return the connection's resources to the pool.
//
// The following example shows how to use a pool in a web application. The
// application creates a pool at application startup and makes it available to
// request handlers using a package level variable. The pool configuration used
// here is an example, not a recommendation.
//
// func newPool(addr string) *redis.Pool {
// return &redis.Pool{
// MaxIdle: 3,
// IdleTimeout: 240 * time.Second,
// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
// }
// }
//
// var (
// pool *redis.Pool
// redisServer = flag.String("redisServer", ":6379", "")
// )
//
// func main() {
// flag.Parse()
// pool = newPool(*redisServer)
// ...
// }
//
// A request handler gets a connection from the pool and closes the connection
// when the handler is done:
//
// func serveHome(w http.ResponseWriter, r *http.Request) {
// conn := pool.Get()
// defer conn.Close()
// ...
// }
//
// Use the Dial function to authenticate connections with the AUTH command or
// select a database with the SELECT command:
//
// pool := &redis.Pool{
// // Other pool configuration not shown in this example.
// Dial: func () (redis.Conn, error) {
// c, err := redis.Dial("tcp", server)
// if err != nil {
// return nil, err
// }
// if _, err := c.Do("AUTH", password); err != nil {
// c.Close()
// return nil, err
// }
// if _, err := c.Do("SELECT", db); err != nil {
// c.Close()
// return nil, err
// }
// return c, nil
// }
// }
//
// Use the TestOnBorrow function to check the health of an idle connection
// before the connection is returned to the application. This example PINGs
// connections that have been idle more than a minute:
//
// pool := &redis.Pool{
// // Other pool configuration not shown in this example.
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
// if time.Since(t) < time.Minute {
// return nil
// }
// _, err := c.Do("PING")
// return err
// },
// }
//
type Pool struct {
// Dial is an application supplied function for creating and configuring a
// connection.
//
// The connection returned from Dial must not be in a special state
// (subscribed to pubsub channel, transaction started, ...).
Dial func() (Conn, error)
// TestOnBorrow is an optional application supplied function for checking
// the health of an idle connection before the connection is used again by
// the application. Argument t is the time that the connection was returned
// to the pool. If the function returns an error, then the connection is
// closed.
TestOnBorrow func(c Conn, t time.Time) error
// Maximum number of idle connections in the pool.
MaxIdle int
// Maximum number of connections allocated by the pool at a given time.
// When zero, there is no limit on the number of connections in the pool.
MaxActive int
// Close connections after remaining idle for this duration. If the value
// is zero, then idle connections are not closed. Applications should set
// the timeout to a value less than the server's timeout.
IdleTimeout time.Duration
// If Wait is true and the pool is at the MaxActive limit, then Get() waits
// for a connection to be returned to the pool before returning.
Wait bool
// mu protects fields defined below.
mu sync.Mutex
cond *sync.Cond
closed bool
active int
// Stack of idleConn with most recently used at the front.
idle list.List
}
type idleConn struct {
c Conn
t time.Time
}
// NewPool creates a new pool.
//
// Deprecated: Initialize the Pool directory as shown in the example.
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
return &Pool{Dial: newFn, MaxIdle: maxIdle}
}
// Get gets a connection. The application must close the returned connection.
// This method always returns a valid connection so that applications can defer
// error handling to the first use of the connection. If there is an error
// getting an underlying connection, then the connection Err, Do, Send, Flush
// and Receive methods return that error.
func (p *Pool) Get() Conn {
c, err := p.get()
if err != nil {
return errorConnection{err}
}
return &pooledConnection{p: p, c: c}
}
// ActiveCount returns the number of active connections in the pool.
func (p *Pool) ActiveCount() int {
p.mu.Lock()
active := p.active
p.mu.Unlock()
return active
}
// Close releases the resources used by the pool.
func (p *Pool) Close() error {
p.mu.Lock()
idle := p.idle
p.idle.Init()
p.closed = true
p.active -= idle.Len()
if p.cond != nil {
p.cond.Broadcast()
}
p.mu.Unlock()
for e := idle.Front(); e != nil; e = e.Next() {
e.Value.(idleConn).c.Close()
}
return nil
}
// release decrements the active count and signals waiters. The caller must
// hold p.mu during the call.
func (p *Pool) release() {
p.active -= 1
if p.cond != nil {
p.cond.Signal()
}
}
// get prunes stale connections and returns a connection from the idle list or
// creates a new connection.
func (p *Pool) get() (Conn, error) {
p.mu.Lock()
// Prune stale connections.
if timeout := p.IdleTimeout; timeout > 0 {
for i, n := 0, p.idle.Len(); i < n; i++ {
e := p.idle.Back()
if e == nil {
break
}
ic := e.Value.(idleConn)
if ic.t.Add(timeout).After(nowFunc()) {
break
}
p.idle.Remove(e)
p.release()
p.mu.Unlock()
ic.c.Close()
p.mu.Lock()
}
}
for {
// Get idle connection.
for i, n := 0, p.idle.Len(); i < n; i++ {
e := p.idle.Front()
if e == nil {
break
}
ic := e.Value.(idleConn)
p.idle.Remove(e)
test := p.TestOnBorrow
p.mu.Unlock()
if test == nil || test(ic.c, ic.t) == nil {
return ic.c, nil
}
ic.c.Close()
p.mu.Lock()
p.release()
}
// Check for pool closed before dialing a new connection.
if p.closed {
p.mu.Unlock()
return nil, errors.New("redigo: get on closed pool")
}
// Dial new connection if under limit.
if p.MaxActive == 0 || p.active < p.MaxActive {
dial := p.Dial
p.active += 1
p.mu.Unlock()
c, err := dial()
if err != nil {
p.mu.Lock()
p.release()
p.mu.Unlock()
c = nil
}
return c, err
}
if !p.Wait {
p.mu.Unlock()
return nil, ErrPoolExhausted
}
if p.cond == nil {
p.cond = sync.NewCond(&p.mu)
}
p.cond.Wait()
}
}
func (p *Pool) put(c Conn, forceClose bool) error {
err := c.Err()
p.mu.Lock()
if !p.closed && err == nil && !forceClose {
p.idle.PushFront(idleConn{t: nowFunc(), c: c})
if p.idle.Len() > p.MaxIdle {
c = p.idle.Remove(p.idle.Back()).(idleConn).c
} else {
c = nil
}
}
if c == nil {
if p.cond != nil {
p.cond.Signal()
}
p.mu.Unlock()
return nil
}
p.release()
p.mu.Unlock()
return c.Close()
}
type pooledConnection struct {
p *Pool
c Conn
state int
}
var (
sentinel []byte
sentinelOnce sync.Once
)
func initSentinel() {
p := make([]byte, 64)
if _, err := rand.Read(p); err == nil {
sentinel = p
} else {
h := sha1.New()
io.WriteString(h, "Oops, rand failed. Use time instead.")
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
sentinel = h.Sum(nil)
}
}
func (pc *pooledConnection) Close() error {
c := pc.c
if _, ok := c.(errorConnection); ok {
return nil
}
pc.c = errorConnection{errConnClosed}
if pc.state&internal.MultiState != 0 {
c.Send("DISCARD")
pc.state &^= (internal.MultiState | internal.WatchState)
} else if pc.state&internal.WatchState != 0 {
c.Send("UNWATCH")
pc.state &^= internal.WatchState
}
if pc.state&internal.SubscribeState != 0 {
c.Send("UNSUBSCRIBE")
c.Send("PUNSUBSCRIBE")
// To detect the end of the message stream, ask the server to echo
// a sentinel value and read until we see that value.
sentinelOnce.Do(initSentinel)
c.Send("ECHO", sentinel)
c.Flush()
for {
p, err := c.Receive()
if err != nil {
break
}
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
pc.state &^= internal.SubscribeState
break
}
}
}
c.Do("")
pc.p.put(c, pc.state != 0)
return nil
}
func (pc *pooledConnection) Err() error {
return pc.c.Err()
}
func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
return pc.c.Do(commandName, args...)
}
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
return pc.c.Send(commandName, args...)
}
func (pc *pooledConnection) Flush() error {
return pc.c.Flush()
}
func (pc *pooledConnection) Receive() (reply interface{}, err error) {
return pc.c.Receive()
}
type errorConnection struct{ err error }
func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
func (ec errorConnection) Send(string, ...interface{}) error { return ec.err }
func (ec errorConnection) Err() error { return ec.err }
func (ec errorConnection) Close() error { return ec.err }
func (ec errorConnection) Flush() error { return ec.err }
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }

View File

@@ -1,684 +0,0 @@
// Copyright 2011 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package redis_test
import (
"errors"
"io"
"reflect"
"sync"
"testing"
"time"
"github.com/garyburd/redigo/redis"
)
type poolTestConn struct {
d *poolDialer
err error
redis.Conn
}
func (c *poolTestConn) Close() error {
c.d.mu.Lock()
c.d.open -= 1
c.d.mu.Unlock()
return c.Conn.Close()
}
func (c *poolTestConn) Err() error { return c.err }
func (c *poolTestConn) Do(commandName string, args ...interface{}) (interface{}, error) {
if commandName == "ERR" {
c.err = args[0].(error)
commandName = "PING"
}
if commandName != "" {
c.d.commands = append(c.d.commands, commandName)
}
return c.Conn.Do(commandName, args...)
}
func (c *poolTestConn) Send(commandName string, args ...interface{}) error {
c.d.commands = append(c.d.commands, commandName)
return c.Conn.Send(commandName, args...)
}
type poolDialer struct {
mu sync.Mutex
t *testing.T
dialed int
open int
commands []string
dialErr error
}
func (d *poolDialer) dial() (redis.Conn, error) {
d.mu.Lock()
d.dialed += 1
dialErr := d.dialErr
d.mu.Unlock()
if dialErr != nil {
return nil, d.dialErr
}
c, err := redis.DialDefaultServer()
if err != nil {
return nil, err
}
d.mu.Lock()
d.open += 1
d.mu.Unlock()
return &poolTestConn{d: d, Conn: c}, nil
}
func (d *poolDialer) check(message string, p *redis.Pool, dialed, open int) {
d.mu.Lock()
if d.dialed != dialed {
d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed)
}
if d.open != open {
d.t.Errorf("%s: open=%d, want %d", message, d.open, open)
}
if active := p.ActiveCount(); active != open {
d.t.Errorf("%s: active=%d, want %d", message, active, open)
}
d.mu.Unlock()
}
func TestPoolReuse(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
Dial: d.dial,
}
for i := 0; i < 10; i++ {
c1 := p.Get()
c1.Do("PING")
c2 := p.Get()
c2.Do("PING")
c1.Close()
c2.Close()
}
d.check("before close", p, 2, 2)
p.Close()
d.check("after close", p, 2, 0)
}
func TestPoolMaxIdle(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
Dial: d.dial,
}
defer p.Close()
for i := 0; i < 10; i++ {
c1 := p.Get()
c1.Do("PING")
c2 := p.Get()
c2.Do("PING")
c3 := p.Get()
c3.Do("PING")
c1.Close()
c2.Close()
c3.Close()
}
d.check("before close", p, 12, 2)
p.Close()
d.check("after close", p, 12, 0)
}
func TestPoolError(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
Dial: d.dial,
}
defer p.Close()
c := p.Get()
c.Do("ERR", io.EOF)
if c.Err() == nil {
t.Errorf("expected c.Err() != nil")
}
c.Close()
c = p.Get()
c.Do("ERR", io.EOF)
c.Close()
d.check(".", p, 2, 0)
}
func TestPoolClose(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
Dial: d.dial,
}
defer p.Close()
c1 := p.Get()
c1.Do("PING")
c2 := p.Get()
c2.Do("PING")
c3 := p.Get()
c3.Do("PING")
c1.Close()
if _, err := c1.Do("PING"); err == nil {
t.Errorf("expected error after connection closed")
}
c2.Close()
c2.Close()
p.Close()
d.check("after pool close", p, 3, 1)
if _, err := c1.Do("PING"); err == nil {
t.Errorf("expected error after connection and pool closed")
}
c3.Close()
d.check("after conn close", p, 3, 0)
c1 = p.Get()
if _, err := c1.Do("PING"); err == nil {
t.Errorf("expected error after pool closed")
}
}
func TestPoolTimeout(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
IdleTimeout: 300 * time.Second,
Dial: d.dial,
}
defer p.Close()
now := time.Now()
redis.SetNowFunc(func() time.Time { return now })
defer redis.SetNowFunc(time.Now)
c := p.Get()
c.Do("PING")
c.Close()
d.check("1", p, 1, 1)
now = now.Add(p.IdleTimeout)
c = p.Get()
c.Do("PING")
c.Close()
d.check("2", p, 2, 1)
}
func TestPoolConcurrenSendReceive(t *testing.T) {
p := &redis.Pool{
Dial: redis.DialDefaultServer,
}
defer p.Close()
c := p.Get()
done := make(chan error, 1)
go func() {
_, err := c.Receive()
done <- err
}()
c.Send("PING")
c.Flush()
err := <-done
if err != nil {
t.Fatalf("Receive() returned error %v", err)
}
_, err = c.Do("")
if err != nil {
t.Fatalf("Do() returned error %v", err)
}
c.Close()
}
func TestPoolBorrowCheck(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
Dial: d.dial,
TestOnBorrow: func(redis.Conn, time.Time) error { return redis.Error("BLAH") },
}
defer p.Close()
for i := 0; i < 10; i++ {
c := p.Get()
c.Do("PING")
c.Close()
}
d.check("1", p, 10, 1)
}
func TestPoolMaxActive(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
MaxActive: 2,
Dial: d.dial,
}
defer p.Close()
c1 := p.Get()
c1.Do("PING")
c2 := p.Get()
c2.Do("PING")
d.check("1", p, 2, 2)
c3 := p.Get()
if _, err := c3.Do("PING"); err != redis.ErrPoolExhausted {
t.Errorf("expected pool exhausted")
}
c3.Close()
d.check("2", p, 2, 2)
c2.Close()
d.check("3", p, 2, 2)
c3 = p.Get()
if _, err := c3.Do("PING"); err != nil {
t.Errorf("expected good channel, err=%v", err)
}
c3.Close()
d.check("4", p, 2, 2)
}
func TestPoolMonitorCleanup(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
MaxActive: 2,
Dial: d.dial,
}
defer p.Close()
c := p.Get()
c.Send("MONITOR")
c.Close()
d.check("", p, 1, 0)
}
func TestPoolPubSubCleanup(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
MaxActive: 2,
Dial: d.dial,
}
defer p.Close()
c := p.Get()
c.Send("SUBSCRIBE", "x")
c.Close()
want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
c = p.Get()
c.Send("PSUBSCRIBE", "x*")
c.Close()
want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
}
func TestPoolTransactionCleanup(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 2,
MaxActive: 2,
Dial: d.dial,
}
defer p.Close()
c := p.Get()
c.Do("WATCH", "key")
c.Do("PING")
c.Close()
want := []string{"WATCH", "PING", "UNWATCH"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
c = p.Get()
c.Do("WATCH", "key")
c.Do("UNWATCH")
c.Do("PING")
c.Close()
want = []string{"WATCH", "UNWATCH", "PING"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
c = p.Get()
c.Do("WATCH", "key")
c.Do("MULTI")
c.Do("PING")
c.Close()
want = []string{"WATCH", "MULTI", "PING", "DISCARD"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
c = p.Get()
c.Do("WATCH", "key")
c.Do("MULTI")
c.Do("DISCARD")
c.Do("PING")
c.Close()
want = []string{"WATCH", "MULTI", "DISCARD", "PING"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
c = p.Get()
c.Do("WATCH", "key")
c.Do("MULTI")
c.Do("EXEC")
c.Do("PING")
c.Close()
want = []string{"WATCH", "MULTI", "EXEC", "PING"}
if !reflect.DeepEqual(d.commands, want) {
t.Errorf("got commands %v, want %v", d.commands, want)
}
d.commands = nil
}
func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error {
errs := make(chan error, 10)
for i := 0; i < cap(errs); i++ {
go func() {
c := p.Get()
_, err := c.Do(cmd, args...)
errs <- err
c.Close()
}()
}
// Wait for goroutines to block.
time.Sleep(time.Second / 4)
return errs
}
func TestWaitPool(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 1,
MaxActive: 1,
Dial: d.dial,
Wait: true,
}
defer p.Close()
c := p.Get()
errs := startGoroutines(p, "PING")
d.check("before close", p, 1, 1)
c.Close()
timeout := time.After(2 * time.Second)
for i := 0; i < cap(errs); i++ {
select {
case err := <-errs:
if err != nil {
t.Fatal(err)
}
case <-timeout:
t.Fatalf("timeout waiting for blocked goroutine %d", i)
}
}
d.check("done", p, 1, 1)
}
func TestWaitPoolClose(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 1,
MaxActive: 1,
Dial: d.dial,
Wait: true,
}
defer p.Close()
c := p.Get()
if _, err := c.Do("PING"); err != nil {
t.Fatal(err)
}
errs := startGoroutines(p, "PING")
d.check("before close", p, 1, 1)
p.Close()
timeout := time.After(2 * time.Second)
for i := 0; i < cap(errs); i++ {
select {
case err := <-errs:
switch err {
case nil:
t.Fatal("blocked goroutine did not get error")
case redis.ErrPoolExhausted:
t.Fatal("blocked goroutine got pool exhausted error")
}
case <-timeout:
t.Fatal("timeout waiting for blocked goroutine")
}
}
c.Close()
d.check("done", p, 1, 0)
}
func TestWaitPoolCommandError(t *testing.T) {
testErr := errors.New("test")
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 1,
MaxActive: 1,
Dial: d.dial,
Wait: true,
}
defer p.Close()
c := p.Get()
errs := startGoroutines(p, "ERR", testErr)
d.check("before close", p, 1, 1)
c.Close()
timeout := time.After(2 * time.Second)
for i := 0; i < cap(errs); i++ {
select {
case err := <-errs:
if err != nil {
t.Fatal(err)
}
case <-timeout:
t.Fatalf("timeout waiting for blocked goroutine %d", i)
}
}
d.check("done", p, cap(errs), 0)
}
func TestWaitPoolDialError(t *testing.T) {
testErr := errors.New("test")
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 1,
MaxActive: 1,
Dial: d.dial,
Wait: true,
}
defer p.Close()
c := p.Get()
errs := startGoroutines(p, "ERR", testErr)
d.check("before close", p, 1, 1)
d.dialErr = errors.New("dial")
c.Close()
nilCount := 0
errCount := 0
timeout := time.After(2 * time.Second)
for i := 0; i < cap(errs); i++ {
select {
case err := <-errs:
switch err {
case nil:
nilCount++
case d.dialErr:
errCount++
default:
t.Fatalf("expected dial error or nil, got %v", err)
}
case <-timeout:
t.Fatalf("timeout waiting for blocked goroutine %d", i)
}
}
if nilCount != 1 {
t.Errorf("expected one nil error, got %d", nilCount)
}
if errCount != cap(errs)-1 {
t.Errorf("expected %d dial errors, got %d", cap(errs)-1, errCount)
}
d.check("done", p, cap(errs), 0)
}
// Borrowing requires us to iterate over the idle connections, unlock the pool,
// and perform a blocking operation to check the connection still works. If
// TestOnBorrow fails, we must reacquire the lock and continue iteration. This
// test ensures that iteration will work correctly if multiple threads are
// iterating simultaneously.
func TestLocking_TestOnBorrowFails_PoolDoesntCrash(t *testing.T) {
const count = 100
// First we'll Create a pool where the pilfering of idle connections fails.
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: count,
MaxActive: count,
Dial: d.dial,
TestOnBorrow: func(c redis.Conn, t time.Time) error {
return errors.New("No way back into the real world.")
},
}
defer p.Close()
// Fill the pool with idle connections.
conns := make([]redis.Conn, count)
for i := range conns {
conns[i] = p.Get()
}
for i := range conns {
conns[i].Close()
}
// Spawn a bunch of goroutines to thrash the pool.
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func() {
c := p.Get()
if c.Err() != nil {
t.Errorf("pool get failed: %v", c.Err())
}
c.Close()
wg.Done()
}()
}
wg.Wait()
if d.dialed != count*2 {
t.Errorf("Expected %d dials, got %d", count*2, d.dialed)
}
}
func BenchmarkPoolGet(b *testing.B) {
b.StopTimer()
p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2}
c := p.Get()
if err := c.Err(); err != nil {
b.Fatal(err)
}
c.Close()
defer p.Close()
b.StartTimer()
for i := 0; i < b.N; i++ {
c = p.Get()
c.Close()
}
}
func BenchmarkPoolGetErr(b *testing.B) {
b.StopTimer()
p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2}
c := p.Get()
if err := c.Err(); err != nil {
b.Fatal(err)
}
c.Close()
defer p.Close()
b.StartTimer()
for i := 0; i < b.N; i++ {
c = p.Get()
if err := c.Err(); err != nil {
b.Fatal(err)
}
c.Close()
}
}
func BenchmarkPoolGetPing(b *testing.B) {
b.StopTimer()
p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2}
c := p.Get()
if err := c.Err(); err != nil {
b.Fatal(err)
}
c.Close()
defer p.Close()
b.StartTimer()
for i := 0; i < b.N; i++ {
c = p.Get()
if _, err := c.Do("PING"); err != nil {
b.Fatal(err)
}
c.Close()
}
}

View File

@@ -1,148 +0,0 @@
// 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{})
}

View File

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

View File

@@ -1,179 +0,0 @@
// 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),
},
}
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"
}

View File

@@ -1,440 +0,0 @@
// 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}
}

View File

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

View File

@@ -1,177 +0,0 @@
// 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

@@ -1,113 +0,0 @@
// 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
}

View File

@@ -1,152 +0,0 @@
// 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

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

View File

@@ -1,17 +0,0 @@
// 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"

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

@@ -0,0 +1,15 @@
language: go
sudo: false
go:
- 1.6.4
- 1.7.4
- tip
git:
depth: 3
script:
- go test -v -covermode=count -coverprofile=coverage.out
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@@ -1,15 +1,19 @@
#Server-Sent Events [![GoDoc](https://godoc.org/github.com/manucorporat/sse?status.svg)](https://godoc.org/github.com/manucorporat/sse) [![Build Status](https://travis-ci.org/manucorporat/sse.svg)](https://travis-ci.org/manucorporat/sse)
# Server-Sent Events
[![GoDoc](https://godoc.org/github.com/gin-contrib/sse?status.svg)](https://godoc.org/github.com/gin-contrib/sse)
[![Build Status](https://travis-ci.org/gin-contrib/sse.svg)](https://travis-ci.org/gin-contrib/sse)
[![codecov](https://codecov.io/gh/gin-contrib/sse/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/sse)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/sse)](https://goreportcard.com/report/github.com/gin-contrib/sse)
Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. The Server-Sent Events EventSource API is [standardized as part of HTML5[1] by the W3C](http://www.w3.org/TR/2009/WD-eventsource-20091029/).
- [Real world demostration using Gin](http://sse.getgin.io/)
- [Read this great SSE introduction by the HTML5Rocks guys](http://www.html5rocks.com/en/tutorials/eventsource/basics/)
- [Browser support](http://caniuse.com/#feat=eventsource)
##Sample code
## Sample code
```go
import "github.com/manucorporat/sse"
import "github.com/gin-contrib/sse"
func httpHandler(w http.ResponseWriter, req *http.Request) {
// data can be a primitive like a string, an integer or a float
@@ -40,7 +44,7 @@ data: {"content":"hi!","date":1431540810,"user":"manu"}
```
##Content-Type
## Content-Type
```go
fmt.Println(sse.ContentType)
@@ -49,6 +53,6 @@ fmt.Println(sse.ContentType)
text/event-stream
```
##Decoding support
## Decoding support
There is a client-side implementation of SSE coming soon.
There is a client-side implementation of SSE coming soon.

View File

@@ -87,13 +87,17 @@ func writeData(w stringWriter, data interface{}) error {
}
func (r Event) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return Encode(w, r)
}
func (r Event) WriteContentType(w http.ResponseWriter) {
header := w.Header()
header["Content-Type"] = contentType
if _, exist := header["Cache-Control"]; !exist {
header["Cache-Control"] = noCache
}
return Encode(w, r)
}
func kindOfData(data interface{}) reflect.Kind {

42
vendor/github.com/gin-gonic/autotls/.editorconfig generated vendored Normal file
View File

@@ -0,0 +1,42 @@
# unifying the coding style for different editors and IDEs => editorconfig.org
; indicate this is the root of the project
root = true
###########################################################
; common
###########################################################
[*]
charset = utf-8
end_of_line = LF
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
###########################################################
; make
###########################################################
[Makefile]
indent_style = tab
[makefile]
indent_style = tab
###########################################################
; markdown
###########################################################
[*.md]
trim_trailing_whitespace = false
###########################################################
; golang
###########################################################
[*.go]
indent_style = tab

View File

@@ -22,3 +22,6 @@ _testmain.go
*.exe
*.test
*.prof
vendor/*
!vendor/vendor.json

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

@@ -0,0 +1,28 @@
language: go
sudo: false
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- master
git:
depth: 3
install:
- go get -v github.com/kardianos/govendor
- govendor sync
- go get -u github.com/campoy/embedmd
script:
- embedmd -d README.md
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false

21
vendor/github.com/gin-gonic/autotls/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Gin-Gonic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

@@ -0,0 +1,67 @@
# autotls
[![Build Status](https://travis-ci.org/gin-gonic/autotls.svg?branch=master)](https://travis-ci.org/gin-gonic/autotls)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/autotls)](https://goreportcard.com/report/github.com/gin-gonic/autotls)
[![GoDoc](https://godoc.org/github.com/gin-gonic/autotls?status.svg)](https://godoc.org/github.com/gin-gonic/autotls)
[![Join the chat at https://gitter.im/gin-gonic/autotls](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/autotls?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Support Let's Encrypt for a Go server application.
## example
example for 1-line LetsEncrypt HTTPS servers.
[embedmd]:# (example/example1.go go)
```go
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
}
```
example for custom autocert manager.
[embedmd]:# (example/example2.go go)
```go
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
Cache: autocert.DirCache("/var/www/.cache"),
}
log.Fatal(autotls.RunWithManager(r, &m))
}
```

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

@@ -0,0 +1,26 @@
package autotls
import (
"crypto/tls"
"net/http"
"golang.org/x/crypto/acme/autocert"
)
// Run support 1-line LetsEncrypt HTTPS servers
func Run(r http.Handler, domain ...string) error {
return http.Serve(autocert.NewListener(domain...), r)
}
// RunWithManager support custom autocert manager
func RunWithManager(r http.Handler, m *autocert.Manager) error {
s := &http.Server{
Addr: ":https",
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
Handler: r,
}
go http.ListenAndServe(":http", m.HTTPHandler(nil))
return s.ListenAndServeTLS("", "")
}

23
vendor/github.com/gin-gonic/autotls/doc.go generated vendored Normal file
View File

@@ -0,0 +1,23 @@
// Package autotls support Let's Encrypt for a Go server application.
//
// package main
//
// import (
// "log"
//
// "github.com/gin-gonic/autotls"
// "github.com/gin-gonic/gin"
// )
//
// func main() {
// r := gin.Default()
//
// // Ping handler
// r.GET("/ping", func(c *gin.Context) {
// c.String(200, "pong")
// })
//
// log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
// }
//
package autotls

View File

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

View File

@@ -1,19 +0,0 @@
language: go
go:
- 1.4.3
- 1.5.4
- 1.6.4
- 1.7.4
- tip
services:
- memcache
- redis-server
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/acc2c57482e94b44f557
on_success: change
on_failure: always
on_start: false

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