mirror of
https://github.com/eiblog/eiblog.git
synced 2026-02-04 13:52:26 +08:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd69c62254 | ||
|
|
970c6068d5 | ||
|
|
27a1f600de | ||
|
|
edf0fbac51 | ||
|
|
98ca570a36 | ||
|
|
9a526f97f8 | ||
|
|
33a29f5e57 | ||
|
|
30ebf76eda | ||
|
|
f5c0bcdb99 | ||
|
|
4b53da3801 | ||
|
|
1bdfb6abea | ||
|
|
33f47d8f3a | ||
|
|
cbd0cfaaf5 | ||
|
|
080c992a92 | ||
|
|
371b2326ea | ||
|
|
2720d11b23 | ||
|
|
b7751d7b9e | ||
|
|
24d81db8be | ||
|
|
010137ebf5 | ||
|
|
c6a2439c54 | ||
|
|
1d54ff3ac5 | ||
|
|
63a4d69209 | ||
|
|
b35d7de58a | ||
|
|
77ea01b7c1 | ||
|
|
5f608b638d | ||
|
|
52da8abceb | ||
|
|
f016b28cb6 | ||
|
|
01b7643ca5 | ||
|
|
375d43761b | ||
|
|
f3e9727947 | ||
|
|
911aa963c7 | ||
|
|
fb66b6871e | ||
|
|
5ae76f243e | ||
|
|
051b034e51 | ||
|
|
27439ecc71 | ||
|
|
d02c838447 | ||
|
|
d17acf5325 | ||
|
|
b278ca377f | ||
|
|
93131441e4 | ||
|
|
ddcc6c2d2e | ||
|
|
ef63ae9598 | ||
|
|
2ed9db5c7b | ||
|
|
06a12bc6f9 | ||
|
|
6524b45751 | ||
|
|
ceb9e2690b | ||
|
|
405fbaf24f | ||
|
|
3245c0e0d3 | ||
|
|
badc62e3f0 | ||
|
|
a5561f257b | ||
|
|
eb37b83ebd | ||
|
|
b2fab703fc | ||
|
|
37deb390d9 | ||
|
|
6fa5088352 | ||
|
|
e023a33786 | ||
|
|
6f818c4b5d | ||
|
|
9ad22fb2d9 | ||
|
|
fc37d5e093 | ||
|
|
61024bfebd | ||
|
|
f20c4a6063 | ||
|
|
c24e6bf7bd | ||
|
|
ade94168d3 | ||
|
|
552d010650 | ||
|
|
1c3106cbb0 | ||
|
|
168937f1b2 | ||
|
|
730cffcb5b | ||
|
|
8c3f1c2aba | ||
|
|
ea375ea76c | ||
|
|
275a6c0c31 | ||
|
|
360204995d | ||
|
|
c9fc0cc75a | ||
|
|
41daaa322e | ||
|
|
894535fbe5 | ||
|
|
6fc5af1b0f | ||
|
|
5ce806a7d7 | ||
|
|
25cb23fdb3 | ||
|
|
a89a1a2bc9 | ||
|
|
93e170f9ac | ||
|
|
59d9a616aa | ||
|
|
2ff0934206 | ||
|
|
cde7cba2f0 | ||
|
|
2be7501afe | ||
|
|
487d35dae2 | ||
|
|
19af9376cb | ||
|
|
3ddd2a0b33 | ||
|
|
ee7523b124 | ||
|
|
cc1dbac1f0 | ||
|
|
04532ba8a6 | ||
|
|
0a2a132b11 | ||
|
|
3ff712d407 | ||
|
|
27162d2205 | ||
|
|
f150974566 | ||
|
|
b94fc825b3 | ||
|
|
d8f0e30285 | ||
|
|
e0a5f0ebca | ||
|
|
c18d9c0bef | ||
|
|
e1ec5cd08a | ||
|
|
5efdd72e58 | ||
|
|
b9470fa14c | ||
|
|
a932d2906d | ||
|
|
3ff5977941 | ||
|
|
da7b726e8d | ||
|
|
3923bc70f1 | ||
|
|
8dc73fd67c | ||
|
|
2825bbfeae | ||
|
|
66811830b0 | ||
|
|
c4bf59ce5d | ||
|
|
6cea283f86 | ||
|
|
3992db49ba | ||
|
|
055c2307cb | ||
|
|
11b0f486cd | ||
|
|
ed0a50b626 | ||
|
|
cf2b2d6d34 | ||
|
|
00456806bb | ||
|
|
54f5289d6b | ||
|
|
1634418a13 | ||
|
|
a84a474504 | ||
|
|
b64cf5985a | ||
|
|
4f9965b6bd | ||
|
|
daffa6c294 | ||
|
|
0bd738438e |
@@ -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
86
.github/workflows/release.yml
vendored
Normal 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
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
*.DS_Store
|
||||
**/.DS_Store
|
||||
*.exe
|
||||
conf/ssl/domain.*
|
||||
eiblog
|
||||
|
||||
45
.travis.yml
45
.travis.yml
@@ -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
|
||||
100
CHANGELOG.md
100
CHANGELOG.md
@@ -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
|
||||
* 修复编辑专题,按钮显示“添加专题”错误
|
||||
* 优化“添加文章”从同步改为异步推送:feed,es,disqus。速度显著提升
|
||||
* (**重要*)头像图片从 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 -> qiniu,name -> 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`设计,安全、极速。
|
||||
|
||||
@@ -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"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
73
Makefile
Normal 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:
|
||||
75
README.md
75
README.md
@@ -1,8 +1,8 @@
|
||||
# EiBlog [](https://travis-ci.org/eiblog/eiblog) [](LICENSE.md) [](https://github.com/eiblog/eiblog/releases)
|
||||
# EiBlog [](https://travis-ci.org/eiblog/eiblog) [](LICENSE.md) [](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+`,堪称完美。这些安全的相关配置会在后面的部署过程中接触到。
|
||||
|
||||
相关图片展示:
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
> `注`:图片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
207
api.go
@@ -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
20
back.go
@@ -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)
|
||||
|
||||
@@ -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
|
||||
7
check.go
7
check.go
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
127
conf/app.yml
127
conf/app.yml
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# like 192.168.99.100:true
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
138
conf/nginx/domain/eiblog.conf
Normal file
138
conf/nginx/domain/eiblog.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# example black list
|
||||
#deny 195.154.211.220;
|
||||
Binary file not shown.
Binary file not shown.
425
db.go
425
db.go
@@ -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
389
disqus.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1
docs/_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -18,3 +18,7 @@ twitter:
|
||||

|
||||
|
||||
可以看到`,`之前是没有内容的,该内容是我们文章的描述。
|
||||
|
||||
### Google OpenSearch
|
||||
在 Chrome 浏览器上,你可以在输入网站后按 TAB 键进入搜索模式,如:
|
||||

|
||||
|
||||
69
docs/autocert.md
Normal file
69
docs/autocert.md
Normal 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"
|
||||
```
|
||||
205
docs/install.md
205
docs/install.md
@@ -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` 的相关部署说明。你如果需要其它方式部署,请参考该方式。
|
||||
|
||||
#### 前提准备
|
||||
这里需要准备一些必要的东西,如果你已准备好。请跳过。
|
||||
|
||||
* `一台服务器`。
|
||||
* `一个域名`,国内服务器需备案。
|
||||
* `有效的证书`。一般使用免费的就可以。如:`Let‘s 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
|
||||
```
|
||||
|
||||
等待些许时间,成功运行。
|
||||
|
||||
|
||||
|
||||
@@ -59,3 +59,16 @@ x 为小写字母(x,y,z)中的 x。使页面未加载时也占了相应的
|
||||

|
||||
```
|
||||
|
||||
### 摘要截取
|
||||
摘要截取主要是提供给首页显示,如:
|
||||

|
||||
|
||||
红框中圈出来的就是截取出来的内容。在 `conf/app.yml` 的配置项有两个:
|
||||
```
|
||||
# 自动截取预览, 字符数
|
||||
length: 400
|
||||
# 截取预览标识
|
||||
identifier: <!--more-->
|
||||
|
||||
```
|
||||
当程序不能检查到 identifier 的标识符时,会采用长度的方式进行截取。
|
||||
|
||||
@@ -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
163
front.go
@@ -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
83
glide.lock
generated
@@ -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: []
|
||||
25
glide.yaml
25
glide.yaml
@@ -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
28
go.mod
Normal 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
99
go.sum
Normal 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=
|
||||
23
helper.go
23
helper.go
@@ -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
64
helper_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
2
model.go
2
model.go
@@ -121,6 +121,8 @@ type Article struct {
|
||||
Excerpt string `bson:"-"`
|
||||
// 一句话描述,文章第一句
|
||||
Desc string `bson:"-"`
|
||||
// disqus thread
|
||||
Thread string `bson:"-"`
|
||||
}
|
||||
|
||||
type SortArticles []*Article
|
||||
|
||||
4
ping.go
4
ping.go
@@ -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)
|
||||
|
||||
97
qiniu.go
97
qiniu.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
53
router.go
53
router.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
25
vendor/github.com/boj/redistore/.travis.yml
generated
vendored
Normal 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 ./...
|
||||
57
vendor/github.com/boj/redistore/README.md
generated
vendored
57
vendor/github.com/boj/redistore/README.md
generated
vendored
@@ -1,12 +1,13 @@
|
||||
# redistore
|
||||
|
||||
[](https://drone.io/github.com/boj/redistore/latest)
|
||||
[](https://godoc.org/github.com/boj/redistore)
|
||||
[](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
7
vendor/github.com/boj/redistore/go.mod
generated
vendored
Normal 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
|
||||
)
|
||||
4
vendor/github.com/boj/redistore/redistore.go
generated
vendored
4
vendor/github.com/boj/redistore/redistore.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
404
vendor/github.com/boj/redistore/redistore_test.go
generated
vendored
404
vendor/github.com/boj/redistore/redistore_test.go
generated
vendored
@@ -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
15
vendor/github.com/davecgh/go-spew/LICENSE
generated
vendored
Normal 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
145
vendor/github.com/davecgh/go-spew/spew/bypass.go
generated
vendored
Normal 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
38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
generated
vendored
Normal 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
341
vendor/github.com/davecgh/go-spew/spew/common.go
generated
vendored
Normal 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
306
vendor/github.com/davecgh/go-spew/spew/config.go
generated
vendored
Normal 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
211
vendor/github.com/davecgh/go-spew/spew/doc.go
generated
vendored
Normal 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
509
vendor/github.com/davecgh/go-spew/spew/dump.go
generated
vendored
Normal 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
419
vendor/github.com/davecgh/go-spew/spew/format.go
generated
vendored
Normal 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
148
vendor/github.com/davecgh/go-spew/spew/spew.go
generated
vendored
Normal 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
1
vendor/github.com/deepzz0/logd/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
149
vendor/github.com/deepzz0/logd/README.md
generated
vendored
Normal file
149
vendor/github.com/deepzz0/logd/README.md
generated
vendored
Normal 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")
|
||||
}
|
||||
```
|
||||
输出如下:
|
||||

|
||||
|
||||
|
||||
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
499
vendor/github.com/deepzz0/logd/logd.go
generated
vendored
Normal 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
80
vendor/github.com/deepzz0/logd/mail.go
generated
vendored
Normal 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()
|
||||
}
|
||||
5
vendor/github.com/garyburd/redigo/.github/CONTRIBUTING.md
generated
vendored
5
vendor/github.com/garyburd/redigo/.github/CONTRIBUTING.md
generated
vendored
@@ -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.
|
||||
1
vendor/github.com/garyburd/redigo/.github/ISSUE_TEMPLATE.md
generated
vendored
1
vendor/github.com/garyburd/redigo/.github/ISSUE_TEMPLATE.md
generated
vendored
@@ -1 +0,0 @@
|
||||
Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis
|
||||
18
vendor/github.com/garyburd/redigo/.travis.yml
generated
vendored
18
vendor/github.com/garyburd/redigo/.travis.yml
generated
vendored
@@ -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 ./...
|
||||
50
vendor/github.com/garyburd/redigo/README.markdown
generated
vendored
50
vendor/github.com/garyburd/redigo/README.markdown
generated
vendored
@@ -1,50 +0,0 @@
|
||||
Redigo
|
||||
======
|
||||
|
||||
[](https://travis-ci.org/garyburd/redigo)
|
||||
[](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).
|
||||
27
vendor/github.com/garyburd/redigo/internal/commandinfo_test.go
generated
vendored
27
vendor/github.com/garyburd/redigo/internal/commandinfo_test.go
generated
vendored
@@ -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")
|
||||
}
|
||||
68
vendor/github.com/garyburd/redigo/internal/redistest/testdb.go
generated
vendored
68
vendor/github.com/garyburd/redigo/internal/redistest/testdb.go
generated
vendored
@@ -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
|
||||
}
|
||||
670
vendor/github.com/garyburd/redigo/redis/conn_test.go
generated
vendored
670
vendor/github.com/garyburd/redigo/redis/conn_test.go
generated
vendored
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
416
vendor/github.com/garyburd/redigo/redis/pool.go
generated
vendored
416
vendor/github.com/garyburd/redigo/redis/pool.go
generated
vendored
@@ -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 }
|
||||
684
vendor/github.com/garyburd/redigo/redis/pool_test.go
generated
vendored
684
vendor/github.com/garyburd/redigo/redis/pool_test.go
generated
vendored
@@ -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()
|
||||
}
|
||||
}
|
||||
148
vendor/github.com/garyburd/redigo/redis/pubsub_test.go
generated
vendored
148
vendor/github.com/garyburd/redigo/redis/pubsub_test.go
generated
vendored
@@ -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{})
|
||||
}
|
||||
41
vendor/github.com/garyburd/redigo/redis/redis.go
generated
vendored
41
vendor/github.com/garyburd/redigo/redis/redis.go
generated
vendored
@@ -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)
|
||||
}
|
||||
179
vendor/github.com/garyburd/redigo/redis/reply_test.go
generated
vendored
179
vendor/github.com/garyburd/redigo/redis/reply_test.go
generated
vendored
@@ -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"
|
||||
}
|
||||
440
vendor/github.com/garyburd/redigo/redis/scan_test.go
generated
vendored
440
vendor/github.com/garyburd/redigo/redis/scan_test.go
generated
vendored
@@ -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}
|
||||
}
|
||||
100
vendor/github.com/garyburd/redigo/redis/script_test.go
generated
vendored
100
vendor/github.com/garyburd/redigo/redis/script_test.go
generated
vendored
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
177
vendor/github.com/garyburd/redigo/redis/test_test.go
generated
vendored
177
vendor/github.com/garyburd/redigo/redis/test_test.go
generated
vendored
@@ -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()
|
||||
}())
|
||||
}
|
||||
113
vendor/github.com/garyburd/redigo/redis/zpop_example_test.go
generated
vendored
113
vendor/github.com/garyburd/redigo/redis/zpop_example_test.go
generated
vendored
@@ -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
|
||||
}
|
||||
152
vendor/github.com/garyburd/redigo/redisx/connmux.go
generated
vendored
152
vendor/github.com/garyburd/redigo/redisx/connmux.go
generated
vendored
@@ -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()
|
||||
}
|
||||
259
vendor/github.com/garyburd/redigo/redisx/connmux_test.go
generated
vendored
259
vendor/github.com/garyburd/redigo/redisx/connmux_test.go
generated
vendored
@@ -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()
|
||||
}
|
||||
17
vendor/github.com/garyburd/redigo/redisx/doc.go
generated
vendored
17
vendor/github.com/garyburd/redigo/redisx/doc.go
generated
vendored
@@ -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
15
vendor/github.com/gin-contrib/sse/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.6.4
|
||||
- 1.7.4
|
||||
- tip
|
||||
|
||||
git:
|
||||
depth: 3
|
||||
|
||||
script:
|
||||
- go test -v -covermode=count -coverprofile=coverage.out
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
@@ -1,15 +1,19 @@
|
||||
#Server-Sent Events [](https://godoc.org/github.com/manucorporat/sse) [](https://travis-ci.org/manucorporat/sse)
|
||||
# Server-Sent Events
|
||||
|
||||
[](https://godoc.org/github.com/gin-contrib/sse)
|
||||
[](https://travis-ci.org/gin-contrib/sse)
|
||||
[](https://codecov.io/gh/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.
|
||||
@@ -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
42
vendor/github.com/gin-gonic/autotls/.editorconfig
generated
vendored
Normal 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
|
||||
3
vendor/qiniupkg.com/x/.gitignore → vendor/github.com/gin-gonic/autotls/.gitignore
generated
vendored
3
vendor/qiniupkg.com/x/.gitignore → vendor/github.com/gin-gonic/autotls/.gitignore
generated
vendored
@@ -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
28
vendor/github.com/gin-gonic/autotls/.travis.yml
generated
vendored
Normal 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
21
vendor/github.com/gin-gonic/autotls/LICENSE
generated
vendored
Normal 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
67
vendor/github.com/gin-gonic/autotls/README.md
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# autotls
|
||||
|
||||
[](https://travis-ci.org/gin-gonic/autotls)
|
||||
[](https://goreportcard.com/report/github.com/gin-gonic/autotls)
|
||||
[](https://godoc.org/github.com/gin-gonic/autotls)
|
||||
[](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
26
vendor/github.com/gin-gonic/autotls/autotls.go
generated
vendored
Normal 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
23
vendor/github.com/gin-gonic/autotls/doc.go
generated
vendored
Normal 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
|
||||
2
vendor/github.com/gin-gonic/contrib/.gitignore
generated
vendored
2
vendor/github.com/gin-gonic/contrib/.gitignore
generated
vendored
@@ -1,2 +0,0 @@
|
||||
*/Godeps/*
|
||||
!*/Godeps/Godeps.json
|
||||
19
vendor/github.com/gin-gonic/contrib/.travis.yml
generated
vendored
19
vendor/github.com/gin-gonic/contrib/.travis.yml
generated
vendored
@@ -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
Reference in New Issue
Block a user