Compare commits

...

66 Commits

Author SHA1 Message Date
deepzz0
52da8abceb update 2018-05-07 20:51:30 +08:00
deepzz0
f016b28cb6 fix comments duration 2018-05-07 20:45:37 +08:00
henry.chen
01b7643ca5 Merge branch 'master' of github.com:eiblog/eiblog 2018-05-07 16:52:56 +08:00
henry.chen
375d43761b let's encrypt v2 embedded ct,rm about cert's ct 2018-05-07 16:51:54 +08:00
Deepzz
f3e9727947 Set theme jekyll-theme-cayman 2018-05-02 09:45:41 +08:00
Deepzz
911aa963c7 Update eiblog.conf
X-Real_IP -> X-Real-IP
2018-03-27 10:48:16 +08:00
henry.chen
fb66b6871e release v1.4.3 2018-02-09 16:15:34 +08:00
henry.chen
5ae76f243e fixed #6,发布文章异步提交,随机 session key等 2018-02-09 13:50:34 +08:00
deepzz0
051b034e51 1. 修复编辑专题:按钮显示"新增专题"错误 2. 编辑专题链接移动到专题名称 2018-02-04 12:39:35 +08:00
Deepzz
27439ecc71 Update install.md 2018-02-01 21:24:00 +08:00
henry.chen
d02c838447 fix archive page bug 2018-01-25 23:09:59 +08:00
Deepzz
d17acf5325 Update amusing.md 2018-01-17 19:16:08 +08:00
deepzz0
b278ca377f update changelog.md 2018-01-14 13:53:32 +08:00
deepzz0
93131441e4 update 2018-01-14 13:38:26 +08:00
deepzz0
ddcc6c2d2e auto archiving by year when the month great than 12 2018-01-14 13:12:59 +08:00
henry.chen
ef63ae9598 fix page archive unable auto update 2018-01-14 02:40:11 +08:00
henry.chen
2ed9db5c7b code logical adjust 2018-01-14 02:02:12 +08:00
deepzz0
06a12bc6f9 update vendor 2018-01-13 18:23:03 +08:00
deepzz0
6524b45751 adjust the code 2018-01-13 18:19:54 +08:00
henry.chen
ceb9e2690b 添加 disqus thread 创建接口 2018-01-13 02:56:35 +08:00
deepzz0
405fbaf24f fix can delete blogroll and about page & fix delete and readd article bug 2018-01-07 20:30:14 +08:00
deepzz0
3245c0e0d3 update vendor & fix upload file url & fix judge file type 2018-01-06 23:24:27 +08:00
Deepzz
badc62e3f0 Update README.md 2018-01-06 11:47:20 +08:00
deepzz0
a5561f257b comment docker-compose.yml backup 2018-01-02 20:21:45 +08:00
deepzz0
eb37b83ebd update README.md 2018-01-01 19:03:16 +08:00
deepzz0
b2fab703fc Merge branch 'master' of github.com:eiblog/eiblog 2018-01-01 18:59:30 +08:00
deepzz0
37deb390d9 docker-compose.yml 添加数据库备份镜像 2018-01-01 18:59:10 +08:00
Deepzz
6fa5088352 更新 ct 服务器地址 2017-12-30 13:50:19 +08:00
Deepzz
e023a33786 Update app.yml
移除 disqus 评论及 Google 分析私人信息配置
2017-12-08 12:19:01 +08:00
henry.chen
6f818c4b5d fix search.html <no value> 2017-12-05 15:08:32 +08:00
henry.chen
9ad22fb2d9 don't use dynamic link: CGO_ENABLED=0 2017-11-30 10:04:54 +08:00
henry.chen
fc37d5e093 fix page:admin/write-post autocomplete tag 2017-11-29 16:17:58 +08:00
henry.chen
61024bfebd update 2017-11-27 18:34:03 +08:00
henry.chen
f20c4a6063 fix docker image: exec user process caused "no such file or directory" 2017-11-27 18:17:41 +08:00
henry.chen
c24e6bf7bd update .travis.yml 2017-11-27 16:43:30 +08:00
henry.chen
ade94168d3 update .travis.yml 2017-11-27 16:32:39 +08:00
henry.chen
552d010650 fix background turn page 2017-11-27 15:21:28 +08:00
deepzz0
1c3106cbb0 update vendor 2017-11-24 22:58:59 +08:00
henry.chen
168937f1b2 fix gopkg.in/mgo import conflict 2017-11-23 13:57:20 +08:00
henry.chen
730cffcb5b 修复文章自动保存bug+发布文章不成功bug 2017-11-17 13:30:47 +08:00
deepzz0
8c3f1c2aba update travis.yml 2017-11-05 13:03:52 +08:00
deepzz0
ea375ea76c update travis.yml 2017-11-05 12:56:46 +08:00
deepzz0
275a6c0c31 update travis.yml 2017-11-05 12:46:01 +08:00
deepzz0
360204995d 使用github的七牛SDK,配置名称Kodo->Qiniu 2017-11-05 12:27:22 +08:00
deepzz0
c9fc0cc75a Merge branch 'master' of github.com:eiblog/eiblog 2017-10-19 20:23:45 +08:00
deepzz0
41daaa322e fix mod date panic 2017-10-19 20:23:36 +08:00
Deepzz
894535fbe5 Update README.md 2017-10-10 20:16:01 -05:00
Deepzz
6fc5af1b0f Update eiblog.conf 2017-09-26 22:42:49 -05:00
henry.chen
5ce806a7d7 挑战 acme.sh 文件验证路径 2017-08-25 18:01:37 +08:00
Deepzz
25cb23fdb3 Update README.md 2017-08-20 17:48:44 +08:00
deepzz0
a89a1a2bc9 update 2017-08-19 14:26:19 +08:00
deepzz0
93e170f9ac fix es/config/scripts 2017-08-19 14:25:28 +08:00
Deepzz
59d9a616aa Update README.md 2017-08-17 17:00:52 +08:00
Deepzz
2ff0934206 Update install.md 2017-08-15 21:31:34 +08:00
Deepzz
cde7cba2f0 Update README.md 2017-08-15 21:23:12 +08:00
Deepzz
2be7501afe Update README.md 2017-08-15 21:12:03 +08:00
deepzz0
487d35dae2 add comments 2017-08-08 20:59:45 +08:00
henry.chen
19af9376cb add comments 2017-08-08 12:45:58 +08:00
deepzz0
3ddd2a0b33 fix disqus 基础评论bug 2017-08-08 01:03:10 +08:00
Deepzz
ee7523b124 Update helper.go 2017-08-07 18:07:58 +08:00
Deepzz
cc1dbac1f0 clean eiblog.conf 2017-07-27 22:03:43 +08:00
deepzz0
04532ba8a6 fix conflict 2017-07-26 22:48:16 +08:00
deepzz0
0a2a132b11 rm some cod in domain.cnf 2017-07-26 22:45:54 +08:00
Deepzz
3ff712d407 Update eiblog.conf 2017-07-25 09:22:08 +08:00
deepzz0
27162d2205 fix unuse tag <!--more-->
intercept errors
2017-07-15 13:46:29 +08:00
deepzz0
f150974566 rm .travis.yml about glide 2017-07-13 21:29:23 +08:00
541 changed files with 33422 additions and 15610 deletions

View File

@@ -1,37 +1,26 @@
sudo: required # 超级权限
dist: trusty # 在ubuntu:trusty
language: go # 声明构建语言环境
go: # 只构建最新版本
- 1.8
services: # docker环境
- tip
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 # 编译版本
- GOOS=linux GOARCH=amd64 CGO_ENABLED=0 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 login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" registry.cn-hangzhou.aliyuncs.com
- docker push registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog
- docker tag registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog:$TRAVIS_TAG
- docker push registry.cn-hangzhou.aliyuncs.com/deepzz/eiblog:$TRAVIS_TAG
before_deploy:
- ./dist.sh
deploy:
provider: releases
api_key:

View File

@@ -1,5 +1,51 @@
# Eiblog Changelog
## v1.4.4 (2018-05-07)
* 修复基础评论分钟数计算错误
* let's encrypt v2证书内嵌ct故移除有关ct内容
## v1.4.3 (2018-02-09)
* 修复博客初始化后about 页面不能够评论 #6
* 修复编辑专题,按钮显示“添加专题”错误
* 优化“添加文章”从同步改为异步推送feedesdisqus。速度显著提升
* **重要*)头像图片从 avatar.jpg 改为 avatar.png透明
* docker-compose.yml mongodb 去掉端口映射,防止用户将端口暴露至外网
* session key 每次重启随机生成等一些细节的修复
## v1.4.2 (2018-01-25)
* fix archive page bug
## v1.4.1 (2018-01-14)
* 修复创建新文章disqus 不收录bug
* 修复创建新文章归档页面不刷新bug
* 修复能够删除关于页面和友情链接页面bug
* 修复重复添加文章错误
* 注释掉 docker-compose.yml 自动备份内容,请自行解开
* 添加当月数大于12归档页面使用年份归档
* 优化代码逻辑
## v1.4.0 (2018-01-01)
* fix 搜索页面 bug
* CGO_ENABLED=0 关闭 cgo
* 更新Makefile ct log 服务器
* 数据库数据终于可以备份了
## v1.3.4 (2017-11-29)
* fix page:admin/write-post autocomplete tag
## v1.3.3 (2017-11-27)
* fix docker image: exec user process caused "no such file or directory"
## v1.3.2 (2017-11-17)
* 修复文章自动保存引起的发布文章不成功的bug
## v1.3.1 (2017-11-05)
* 修复调整 关于、友情链接 创建时间出现文章乱序
* 修复评论时间计算错误
* 调整acme文件验证路径
* 更改七牛SDK包为github包。
* 调整七牛配置文件名称app.yml: kodo -> qiniuname -> bucket请提高静态文件版本 staticversion
## v1.3.0 (2017-07-13)
* 更改 app.yml 配置项,将大部分配置归在 general 常规配置下。注意,部署时请先更新 app.yml。
* 静态文件采用动态渲染,即用户不再需要管理 view、static 目录。

View File

@@ -7,4 +7,4 @@ ADD static/tzdata/Shanghai /etc/localtime
COPY . /eiblog
EXPOSE 9000
WORKDIR /eiblog
CMD ["./eiblog"]
CMD ["sh","-c","/eiblog/eiblog"]

View File

@@ -15,7 +15,7 @@ test:
build:
@echo "go build..."
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build && \
docker build -t $(docker_registry)/deepzz/eiblog:latest .
docker build -t $(docker_registry)/deepzz/eiblog:latest .
deploy:build
@docker push $(docker_registry)/deepzz/eiblog:latest
@@ -24,47 +24,33 @@ dist:
@./dist.sh
gencert:makedir
@echo $(Ali_Key) $(Ali_Secret)
@if [ ! -n "$(sans)" ]; then \
printf "Need one argument [sans=params]\n"; \
printf "example: sans=\"-d domain -d domain\"\n"; \
exit 1; \
fi; \
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
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; \
curl https://get.acme.sh | sh; \
fi
@echo "generate rsa cert..."
@$(acme.sh) --force --issue --dns dns_ali \
$(sans) --log --renew-hook "ct-submit ctlog.api.venafi.com < $(config)/ssl/domain.rsa.pem > $(config)/scts/rsa/venafi.sct && \
ct-submit ctlog.wosign.com < $(config)/ssl/domain.rsa.pem > $(config)/scts/rsa/wosign.sct"
@$(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"
@$(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 --log --renew-hook "ct-submit ctlog.api.venafi.com < $(config)/ssl/domain.ecc.pem > $(config)/scts/ecc/venafi.sct && \
ct-submit ctlog.wosign.com < $(config)/ssl/domain.ecc.pem > $(config)/scts/ecc/wosign.sct"
@$(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"
# fullchained:
# @if [ ! -n "$(cn)" ]; then \
# printf "Use acme.sh generated certs, Need one argument [cn=params]\n"; \
# printf "example: cn=domain\n"; \
# exit 1; \
# fi
# @cp $(acme)/$(cn)/ca.cer $(config)/ssl/full_chained.pem && \
# echo $(X3) >> $(config)/ssl/full_chained.pem
@$(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
@@ -73,7 +59,7 @@ ssticket:
@openssl rand 48 > $(config)/ssl/session_ticket.key
makedir:
@mkdir -p $(config)/ssl $(config)/scts/rsa $(config)/scts/ecc
@mkdir -p $(config)/ssl
clean:

View File

@@ -1,4 +1,4 @@
# EiBlog [![Build Status](https://travis-ci.org/eiblog/eiblog.svg?branch=master)](https://travis-ci.org/eiblog/eiblog) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) [![Versuib](https://img.shields.io/github/tag/eiblog/eiblog.svg)](https://github.com/eiblog/eiblog/releases)
# EiBlog [![Build Status](https://travis-ci.org/eiblog/eiblog.svg?branch=v1.3.0)](https://travis-ci.org/eiblog/eiblog) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) [![Versuib](https://img.shields.io/github/tag/eiblog/eiblog.svg)](https://github.com/eiblog/eiblog/releases)
> 系统根据[https://imququ.com](https://imququ.com)一系列文章和方向进行搭建期间获得了QuQu的很大帮助在此表示感谢。
@@ -11,8 +11,8 @@
整个博客系统涉及到模块如下:
* 自动更新证书:
* 接入 [acme/autocert](https://github.com/golang/crypto/tree/master/acme/autocert),在 TLS 层开启全自动更新证书,从此证书的更新再也不用惦记了。
* 如果你采用如 Nginx 代理,你可能需要 [acme.sh](https://github.com/Neilpang/acme.sh) 实现证书的自动。博主实现 aliyun dns 的自动验证方式,详见 [Makefile/gencert](https://github.com/eiblog/eiblog/blob/master/Makefile)。
* 接入 [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` 作为博客的站内搜索,尽管占用内存稍高。
* `Disqus`,作为博客评论系统,国内大部分被墙,故实现两种评论方式。
@@ -65,8 +65,8 @@
* `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 调用,又再次达到加速访问的目的。
@@ -74,6 +74,7 @@
8. 开源 `Typecho` 完整后台系统,全功能 `markdown` 编辑器,让你体验什么是简洁清爽。
9. 博客后台直接对接 `七牛 SDK`,实现后台上传文件和删除文件的简单功能。
10. 采用 `elasticsearch` 作为站内搜索,添加 `google opensearch` 功能,搜索更加自然。
11. 自动备份数据库数据到七牛云。
### 文档
@@ -81,9 +82,10 @@
* [安装部署](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://razeen.me](https://razeen.me) - Razeen's Blog
如果你的博客使用`Eiblog`搭建,你可以在 [这里](https://github.com/eiblog/eiblog/issues/1) 提交网址。

207
api.go
View File

@@ -4,23 +4,26 @@ import (
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
"github.com/eiblog/utils/mgo"
"github.com/gin-gonic/gin"
"gopkg.in/mgo.v2/bson"
)
const (
// 成功
NOTICE_SUCCESS = "success"
NOTICE_NOTICE = "notice"
NOTICE_ERROR = "error"
// 注意
NOTICE_NOTICE = "notice"
// 错误
NOTICE_ERROR = "error"
)
// 全局 API
var APIs = make(map[string]func(c *gin.Context))
func init() {
@@ -52,6 +55,7 @@ func init() {
APIs["file-delete"] = apiFileDelete
}
// 更新账号信息Email、PhoneNumber、Address
func apiAccount(c *gin.Context) {
e := c.PostForm("email")
pn := c.PostForm("phoneNumber")
@@ -61,8 +65,10 @@ func apiAccount(c *gin.Context) {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
return
}
err := UpdateAccountField(bson.M{"$set": bson.M{"email": e, "phonen": pn, "address": ad}})
err := UpdateAccountField(mgo.M{"$set": mgo.M{"email": e, "phonen": pn, "address": ad}})
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
@@ -72,6 +78,7 @@ func apiAccount(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "更新成功", "")
}
// 更新博客信息
func apiBlog(c *gin.Context) {
bn := c.PostForm("blogName")
bt := c.PostForm("bTitle")
@@ -83,8 +90,12 @@ func apiBlog(c *gin.Context) {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
return
}
err := UpdateAccountField(bson.M{"$set": bson.M{"blogger.blogname": bn, "blogger.btitle": bt, "blogger.beian": ba, "blogger.subtitle": st, "blogger.seriessay": ss, "blogger.archivessay": as}})
err := UpdateAccountField(mgo.M{"$set": mgo.M{"blogger.blogname": bn,
"blogger.btitle": bt, "blogger.beian": ba, "blogger.subtitle": st,
"blogger.seriessay": ss, "blogger.archivessay": as}})
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
@@ -99,6 +110,7 @@ func apiBlog(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "更新成功", "")
}
// 更新密码
func apiPassword(c *gin.Context) {
logd.Debug(c.Request.PostForm.Encode())
od := c.PostForm("old")
@@ -117,8 +129,10 @@ func apiPassword(c *gin.Context) {
return
}
newPwd := EncryptPasswd(Ei.Username, nw)
err := UpdateAccountField(bson.M{"$set": bson.M{"password": newPwd}})
err := UpdateAccountField(mgo.M{"$set": mgo.M{"password": newPwd}})
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
@@ -126,45 +140,39 @@ func apiPassword(c *gin.Context) {
responseNotice(c, NOTICE_SUCCESS, "更新成功", "")
}
// 删除文章,软删除:移入到回收箱
func apiPostDelete(c *gin.Context) {
var err error
defer func() {
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}()
err = c.Request.ParseForm()
if err != nil {
return
}
var ids []int32
var i int
for _, v := range c.Request.PostForm["cid[]"] {
i, err = strconv.Atoi(v)
if err != nil || i < 1 {
err = errors.New("参数错误")
for _, v := range c.PostFormArray("cid[]") {
i, err := strconv.Atoi(v)
if err != nil || int32(i) < setting.Conf.General.StartID {
responseNotice(c, NOTICE_NOTICE, "参数错误", "")
return
}
ids = append(ids, int32(i))
}
err = DelArticles(ids...)
err := DelArticles(ids...)
if err != nil {
logd.Error(err)
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
// elasticsearch 删除索引
// elasticsearch
err = ElasticDelIndex(ids)
if err != nil {
return
logd.Error(err)
}
// TODO disqus delete
responseNotice(c, NOTICE_SUCCESS, "删除成功", "")
}
func apiPostAdd(c *gin.Context) {
var err error
var do string
var cid int
var (
err error
do string
cid int
)
defer func() {
switch do {
case "auto": // 自动保存
@@ -173,29 +181,28 @@ func apiPostAdd(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{"success": SUCCESS, "time": time.Now().Format("15:04:05 PM"), "cid": cid})
case "save": // 保存草稿
case "save", "publish": // 草稿,发布
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
}
c.Redirect(http.StatusFound, "/admin/manage-draft")
case "publish": // 发布
if err != nil {
responseNotice(c, NOTICE_NOTICE, err.Error(), "")
return
uri := "/admin/manage-draft"
if do == "publish" {
uri = "/admin/manage-posts"
}
c.Redirect(http.StatusFound, "/admin/manage-posts")
c.Redirect(http.StatusFound, uri)
}
}()
do = c.PostForm("do") // auto or save or publish
slug := c.PostForm("slug")
title := c.PostForm("title")
text := c.PostForm("text")
date := c.PostForm("date")
date := CheckDate(c.PostForm("date"))
serie := c.PostForm("serie")
tag := c.PostForm("tags")
update := c.PostForm("update")
if title == "" || text == "" || slug == "" {
if slug == "" || title == "" || text == "" {
err = errors.New("参数错误")
return
}
@@ -208,13 +215,14 @@ func apiPostAdd(c *gin.Context) {
Title: title,
Content: text,
Slug: slug,
CreateTime: CheckDate(date),
CreateTime: date,
IsDraft: do != "publish",
Author: Ei.Username,
SerieID: serieid,
Tags: tags,
}
cid, err = strconv.Atoi(c.PostForm("cid"))
// 新文章
if err != nil || cid < 1 {
err = AddArticle(artc)
if err != nil {
@@ -223,58 +231,55 @@ func apiPostAdd(c *gin.Context) {
}
cid = int(artc.ID)
if !artc.IsDraft {
ElasticIndex(artc)
DoPings(slug)
// 异步执行,快
go func() {
// elastic
ElasticIndex(artc)
// rss
DoPings(slug)
// disqus
ThreadCreate(artc)
}()
}
return
}
// 旧文章
artc.ID = int32(cid)
i, a := GetArticle(artc.ID)
_, a := GetArticle(artc.ID)
if a != nil {
artc.IsDraft = false
artc.Count = a.Count
artc.UpdateTime = a.UpdateTime
Ei.Articles = append(Ei.Articles[0:i], Ei.Articles[i+1:]...)
DelFromLinkedList(a)
ManageTagsArticle(a, false, DELETE)
ManageSeriesArticle(a, false, DELETE)
ManageArchivesArticle(a, false, DELETE)
delete(Ei.MapArticles, a.Slug)
a = nil
}
if CheckBool(update) {
artc.UpdateTime = time.Now()
}
err = UpdateArticle(bson.M{"id": artc.ID}, artc)
// 数据库更新
err = UpdateArticle(mgo.M{"id": artc.ID}, artc)
if err != nil {
logd.Error(err)
return
}
if !artc.IsDraft {
Ei.MapArticles[artc.Slug] = artc
Ei.Articles = append(Ei.Articles, artc)
sort.Sort(Ei.Articles)
GenerateExcerptAndRender(artc)
// elasticsearch 索引
ElasticIndex(artc)
DoPings(slug)
if artc.ID >= setting.Conf.General.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) {

18
back.go
View File

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

View File

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

View File

@@ -35,13 +35,14 @@ general:
clean: 1
# 评论相关
disqus:
shortname: deepzz
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
# disqus.js 文件名
embed: disqus_7d3cf2.js
# 获取评论数量间隔
@@ -49,12 +50,12 @@ disqus:
# 谷歌统计
google:
url: https://www.google-analytics.com/collect
tid: UA-77251712-1
tid: UA-xxxxxx-1
v: "1"
t: pageview
# 七牛CDN
kodo:
name: eiblog
qiniu:
bucket: eiblog
domain: st.deepzz.com
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf

View File

View File

@@ -9,9 +9,11 @@ server {
# ip 黑名单
include /data/eiblog/conf/nginx/ip.blacklist;
# 现在一般证书是内置的。letsencrypt 暂未
# letsencrypt v2已内置
# https://imququ.com/post/certificate-transparency.html#toc-2
ssl_ct on;
#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
@@ -20,10 +22,8 @@ server {
# 站点证书 + 中间证书,私钥
ssl_certificate /data/eiblog/conf/ssl/domain.rsa.pem;
ssl_certificate_key /data/eiblog/conf/ssl/domain.rsa.key;
ssl_ct_static_scts /data/eiblog/conf/scts/rsa/;
# ssl_certificate /data/eiblog/conf/ssl/domain.ecc.pem;
# ssl_certificate_key /data/eiblog/conf/ssl/domain.ecc.key;
# ssl_ct_static_scts /data/eiblog/conf/scts/ecc/;
# openssl dhparam -out dhparams.pem 2048
# https://weakdh.org/sysadmin.html
@@ -65,28 +65,6 @@ server {
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;
@@ -95,7 +73,7 @@ server {
# 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.2.1;
add_header X-Powered-By eiblog/1.3.0;
add_header X-Content-Type-Options nosniff;
proxy_set_header Connection "";
@@ -116,19 +94,18 @@ server {
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";
# 中间证书证书指纹
# 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; includeSubDomains';
add_header Public-Key-Pins 'pin-sha256="IiSbZ4pMDEyXvtl7Lg8K3FNmJcTAhKUTrB2FQOaAO/s="; pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; max-age=2592000;';
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_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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9000;
@@ -147,7 +124,7 @@ server {
# letsencrypt file verify
location ^~ /.well-known/acme-challenge/ {
alias /data/letsencrypt/challenges/;
alias /data/eiblog/challenges/;
try_files $uri =404;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

406
db.go
View File

@@ -13,9 +13,7 @@ import (
"github.com/eiblog/blackfriday"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/utils/logd"
db "github.com/eiblog/utils/mgo"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"github.com/eiblog/utils/mgo"
)
// 数据库及表名
@@ -62,44 +60,24 @@ var (
func init() {
// 数据库加索引
ms, c := db.Connect(DB, COLLECTION_ACCOUNT)
index := mgo.Index{
Key: []string{"username"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err := c.EnsureIndex(index); err != nil {
err := mgo.Index(DB, COLLECTION_ACCOUNT, []string{"username"})
if err != nil {
logd.Fatal(err)
}
ms.Close()
ms, c = db.Connect(DB, COLLECTION_ARTICLE)
index = mgo.Index{
Key: []string{"id"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err := c.EnsureIndex(index); err != nil {
err = mgo.Index(DB, COLLECTION_ARTICLE, []string{"id"})
if err != nil {
logd.Fatal(err)
}
index = mgo.Index{
Key: []string{"slug"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err := c.EnsureIndex(index); err != nil {
err = mgo.Index(DB, COLLECTION_ARTICLE, []string{"slug"})
if err != nil {
logd.Fatal(err)
}
ms.Close()
// 读取帐号信息
Ei = loadAccount()
loadAccount()
// 获取文章数据
Ei.Articles = loadArticles()
loadArticles()
// 生成markdown文档
go generateMarkdown()
// 启动定时器
@@ -109,12 +87,13 @@ func init() {
}
// 读取或初始化帐号信息
func loadAccount() (a *Account) {
a = &Account{}
err := db.FindOne(DB, COLLECTION_ACCOUNT, bson.M{"username": setting.Conf.Account.Username}, a)
func loadAccount() {
Ei = &Account{}
err := mgo.FindOne(DB, COLLECTION_ACCOUNT, mgo.M{"username": setting.Conf.Account.Username}, Ei)
// 初始化用户数据
if err == mgo.ErrNotFound {
a = &Account{
logd.Printf("Initializing account: %s\n", setting.Conf.Account.Username)
Ei = &Account{
Username: setting.Conf.Account.Username,
Password: EncryptPasswd(setting.Conf.Account.Username, setting.Conf.Account.Password),
Email: setting.Conf.Account.Email,
@@ -122,29 +101,28 @@ 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
@@ -153,18 +131,15 @@ func loadArticles() (artcs SortArticles) {
continue
}
if i > 0 {
v.Prev = artcs[i-1]
v.Prev = Ei.Articles[i-1]
}
if artcs[i+1].ID >= setting.Conf.General.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)
}
@@ -273,94 +268,6 @@ 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.General.Identifier)
@@ -374,6 +281,7 @@ func GenerateExcerptAndRender(artc *Article) {
artc.Content = artc.Content[index:]
}
// 查找目录
content := renderPage([]byte(artc.Content))
index := regH.FindIndex(content)
if index != nil {
@@ -397,44 +305,151 @@ 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) {
if oldArtc != nil {
i, artc := GetArticle(oldArtc.ID)
DelFromLinkedList(artc)
Ei.Articles = append(Ei.Articles[:i], Ei.Articles[i+1:]...)
delete(Ei.MapArticles, artc.Slug)
dropArticle(oldArtc)
}
Ei.MapArticles[newArtc.Slug] = newArtc
Ei.Articles = append(Ei.Articles, newArtc)
sort.Sort(Ei.Articles)
GenerateExcerptAndRender(newArtc)
AddToLinkedList(newArtc.ID)
upArticle(newArtc, true)
}
// 添加文章
func AddArticle(artc *Article) error {
// 分配ID, 占位至起始id
for {
if id := db.NextVal(DB, COUNTER_ARTICLE); id < setting.Conf.General.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 +461,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,6 +483,7 @@ func DelFromLinkedList(artc *Article) {
}
}
// 将文章添加到链表
func AddToLinkedList(id int32) {
i, artc := GetArticle(id)
if i == 0 && Ei.Articles[i+1].ID >= setting.Conf.General.StartID {
@@ -501,34 +514,36 @@ func timer() {
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.General.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 +551,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 +572,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 +596,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)
}

249
disqus.go
View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
@@ -17,7 +18,14 @@ import (
var ErrDisqusConfig = errors.New("disqus config incorrect")
type result struct {
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
@@ -26,11 +34,8 @@ type result struct {
}
}
// 定时获取所有文章评论数量
func PostsCount() error {
if setting.Conf.Disqus.PostsCount == "" ||
setting.Conf.Disqus.PublicKey == "" ||
setting.Conf.Disqus.ShortName == "" {
if !correctDisqusConfig() {
return ErrDisqusConfig
}
@@ -41,20 +46,19 @@ func PostsCount() error {
}
})
baseUrl := setting.Conf.Disqus.PostsCount +
"?api_key=" + setting.Conf.Disqus.PublicKey +
"&forum=" + setting.Conf.Disqus.ShortName + "&"
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) {
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 := http.Get(setting.Conf.Disqus.PostsCount + "?" + vals.Encode())
if err != nil {
return err
}
@@ -68,12 +72,12 @@ func PostsCount() error {
return errors.New(string(b))
}
rst := result{}
err = json.Unmarshal(b, &rst)
result := &postsCountResp{}
err = json.Unmarshal(b, result)
if err != nil {
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 {
@@ -86,43 +90,50 @@ func PostsCount() error {
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
}
}
Thread string
}
Response []postDetail
}
// 获取文章评论列表
func PostsList(slug, cursor string) (*postsList, error) {
if setting.Conf.Disqus.PostsList == "" ||
setting.Conf.Disqus.PublicKey == "" ||
setting.Conf.Disqus.ShortName == "" {
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
}
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 := http.Get(setting.Conf.Disqus.PostsList + "?" + vals.Encode())
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
@@ -130,90 +141,91 @@ func PostsList(slug, cursor string) (*postsList, error) {
if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(b))
}
pl := &postsList{}
err = json.Unmarshal(b, pl)
result := &postsListResp{}
err = json.Unmarshal(b, result)
if err != nil {
return nil, err
}
return pl, nil
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 PostComment struct {
Message string
Parent string
Thread string
AuthorEmail string
AuthorName string
IpAddress string
Identifier string
UserAgent string
}
type PostResponse struct {
Code int `json:"code"`
Response struct {
Id string `json:"id"`
} `json:"response"`
type postCreateResp struct {
Code int
Response postDetail
}
// 评论文章
func PostComment(pc *PostCreate) (string, error) {
if setting.Conf.Disqus.PostsList == "" ||
setting.Conf.Disqus.PublicKey == "" ||
setting.Conf.Disqus.ShortName == "" {
return "", ErrDisqusConfig
func PostCreate(pc *PostComment) (*postCreateResp, error) {
if !correctDisqusConfig() {
return nil, 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", "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")
request, err := http.NewRequest("POST", setting.Conf.Disqus.PostCreate, strings.NewReader(vals.Encode()))
if err != nil {
return "", err
return nil, err
}
request.Header.Set("Referer", "https://disqus.com")
resp, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
return nil, err
}
if resp.StatusCode != http.StatusOK {
return "", errors.New(string(b))
}
pr := &PostResponse{}
err = json.Unmarshal(b, pr)
if err != nil {
return "", err
}
return pr.Response.Id, nil
}
type ApprovedResponse struct {
Code int `json:"code"`
Response []struct {
Id string `json:"id"`
} `json:"response"`
if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(b))
}
result := &postCreateResp{}
err = json.Unmarshal(b, result)
if err != nil {
return nil, err
}
return result, nil
}
// 批准评论通过
type approvedResp struct {
Code int
Response []struct {
Id string
}
}
func PostApprove(post string) error {
if setting.Conf.Disqus.PostsList == "" ||
setting.Conf.Disqus.PublicKey == "" ||
setting.Conf.Disqus.ShortName == "" {
if !correctDisqusConfig() {
return ErrDisqusConfig
}
url := setting.Conf.Disqus.PostApprove +
"?api_key=" + setting.Conf.Disqus.PublicKey +
"&access_token=" + setting.Conf.Disqus.AccessToken +
"&post=" + post
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("post", post)
request, err := http.NewRequest("POST", setting.Conf.Disqus.PostApprove, strings.NewReader(vals.Encode()))
if err != nil {
return err
}
@@ -223,6 +235,7 @@ func PostApprove(post string) error {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
@@ -232,11 +245,57 @@ func PostApprove(post string) error {
return errors.New(string(b))
}
ar := &ApprovedResponse{}
err = json.Unmarshal(b, ar)
result := &approvedResp{}
err = json.Unmarshal(b, result)
if err != nil {
return err
}
return nil
}
// 创建thread
type threadCreateResp struct {
Code int
Response struct {
Id string
}
}
func ThreadCreate(artc *Article) 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("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 := http.PostForm(setting.Conf.Disqus.ThreadCreate, vals)
if err != nil {
return err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New(string(b))
}
result := &threadCreateResp{}
err = json.Unmarshal(b, result)
if err != nil {
return err
}
artc.Thread = result.Response.Id
return nil
}

View File

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

View File

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

1
docs/_config.yml Normal file
View File

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

View File

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

View File

@@ -6,7 +6,7 @@ $ curl -L https://github.com/eiblog/eiblog/releases/download/v1.0.0/eiblog-v1.0.
2、如果有幸你也是 `Gopher`,相信你会亲自动手,你可以通过:
``` sh
$ go get -u https://github.com/eiblog/eiblog
$ git clone https://github.com/eiblog/eiblog.git
```
进行源码编译二进制文件运行。
@@ -90,12 +90,14 @@ $ docker run -d --name eisearch \
| ------------------ | ---------------------------------------- | ---------------------------------------- |
| 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.jpg | st.example.com/static/img/avatar.jpg | 头像 |
| 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) |
| disqus.js | st.example.com/static/js/disqus_xxx.js | disqus 文件,你可以通过 https://short_name.disqus.com/embed.js 下载你的专属文件,并上传到七牛。更新配置文件 app.yml。 |
> 注意:每次修改 views 内的以 `st_` 开头的文件,请将 `app.yml` 中的 staticversion 提高一个版本。 cdn 提到的文件下载,请复制链接进行下载,因为博主使用了防盗链功能
> 注意cdn 提到的文件下载,请复制链接进行下载,因为博主使用了防盗链功能,还有:
 1、每次修改 app.yml 文件(如:更换 cdn 域名或更新头像),如果你不知道是否应该提高 staticversion 一个版本,那么最好提高一个 +1。
2、每次手动修改 views 内的以 `st_` 开头的文件,请将 `app.yml` 中的 staticversion 提高一个版本。
#### 配置说明
走到这里,我相信只走到 `60%` 的路程。放弃还来得及。
@@ -141,7 +143,7 @@ $ docker run -d --name eisearch \
| ----------- | ---------------------------------------- |
| app.yml | 整个程序的配置文件,里面已经列出了所有配置项的说明,这里不再阐述。 |
| blackip.yml | 如果没有使用 `Nginx`,博客内置 `ip` 过滤系统。 |
| es | elasticsearch非常强大的分布式搜索引擎`github` 用的就是它。里面的配置基本不用修改,但 `es/analysis/synonym.txt` 是同义词,你可以照着已有的随意增加。注意,scripts 虽然是空的,但请保持存在。 |
| 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 | 这里存放了所有证书相关的内容。 |
@@ -186,8 +188,6 @@ $ docker run -d --name eiblog --restart=always \
首先,请将本地测试好的 `conf``docker-compose.yml` 文件夹和文件上传至服务器。`conf` 建议存储到服务器 `/data/eiblog` 下,`docker-compose.yml` 存放在你使用方便的地方。
> 注意检查 `conf/es/config/scripts` 空文件夹是否存在,不存在即创建。
``` sh
$ tree /data/eiblog -L 1

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"regexp"
"strings"
@@ -23,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() {
_, 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))
@@ -38,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)
@@ -89,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,
@@ -109,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))
@@ -127,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 {
@@ -152,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) {
@@ -187,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 {
@@ -213,6 +240,7 @@ type ESDeleteResult struct {
} `json:"iterms"`
}
// 删除文档
func DeleteDocument(index, typ string, ids []string) error {
var buff bytes.Buffer
for _, id := range ids {
@@ -247,6 +275,7 @@ func DeleteDocument(index, typ string, ids []string) error {
return nil
}
// 查询结果
type ESSearchResult struct {
Took float32 `json:"took"`
Hits struct {
@@ -268,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 {

View File

@@ -86,11 +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)
@@ -101,6 +102,7 @@ func HandleNotFound(c *gin.Context) {
RenderHTMLFront(c, "notfound", h)
}
// 首页
func HandleHomePage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -117,6 +119,7 @@ func HandleHomePage(c *gin.Context) {
RenderHTMLFront(c, "home", h)
}
// 专题页
func HandleSeriesPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -129,6 +132,7 @@ func HandleSeriesPage(c *gin.Context) {
RenderHTMLFront(c, "series", h)
}
// 归档页
func HandleArchivesPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -141,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 {
@@ -178,6 +183,7 @@ func HandleArticlePage(c *gin.Context) {
RenderHTMLFront(c, name, h)
}
// 搜索页
func HandleSearchPage(c *gin.Context) {
h := GetBase()
h["Version"] = StaticVersion(c)
@@ -193,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.General.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 {
@@ -220,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] == "" {
@@ -240,26 +249,32 @@ 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")
}
@@ -321,9 +336,9 @@ 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) {
@@ -356,30 +371,39 @@ 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,
}
}
}
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{
pc := &PostComment{
Message: msg,
Parent: c.PostForm("parent"),
Thread: thread,
@@ -389,24 +413,34 @@ func HandleDisqusCreate(c *gin.Context) {
IpAddress: c.ClientIP(),
}
id, err := PostComment(pc)
postDetail, err := PostCreate(pc)
if err != nil {
logd.Error(err)
rep["errno"] = FAIL
rep["errmsg"] = "系统错误"
resp.ErrNo = FAIL
resp.ErrMsg = "系统错误"
return
}
err = PostApprove(id)
err = PostApprove(postDetail.Response.Id)
if err != nil {
logd.Error(err)
rep["errno"] = FAIL
rep["errmsg"] = "系统错误"
resp.ErrNo = FAIL
resp.ErrMsg = "系统错误"
return
}
rep["errno"] = SUCCESS
rep["data"] = gin.H{"id": id}
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)

43
glide.lock generated
View File

@@ -1,28 +1,28 @@
hash: bd360fa297ed66950543990f9433cdcdf13c29dd99d9a01b49027e236b2cb9da
updated: 2017-07-13T01:29:28.226895963+08:00
hash: c733fa4abeda21b59b001578b37a168bd33038d337b61198cc5fd94be8bfdf77
updated: 2018-01-13T18:22:28.620808+08:00
imports:
- name: github.com/boj/redistore
version: 4562487a4bee9a7c272b72bfaeda4917d0a47ab9
- name: github.com/deepzz0/logd
version: 2bbe53d047054777f3a171cdfc6dca7aa9f8af78
version: f91dd8c6316f0e156e93895a96739b67577b6a63
- name: github.com/eiblog/blackfriday
version: c0ec111761ae784fe31cc076f2fa0e2d2216d623
- name: github.com/eiblog/utils
version: ddfd888542f9a093000f71c3709009c1440a0789
version: e8f16268dae939f920ddc55f1c9e46a97a5e3559
subpackages:
- logd
- mgo
- tmpl
- uuid
- name: github.com/garyburd/redigo
version: 9f3a0116c9f72c5a56f958206a43dc881b502c37
version: d1ed5c67e5794de818ea85e6b522fda02623a484
subpackages:
- internal
- redis
- name: github.com/gin-gonic/autotls
version: 9261e1c52a0eb595c531ff77c06cdfb6fdb111a4
version: 8ca25fbde72bb72a00466215b94b489c71fcb815
- name: github.com/gin-gonic/contrib
version: d4fc5a96cc0d29cb0e862bb1312dd6f4fedfcaee
version: 88aede40372d4bcb11e45168a8c30d99e44cf617
subpackages:
- sessions
- name: github.com/gin-gonic/gin
@@ -39,32 +39,29 @@ imports:
- name: github.com/gorilla/securecookie
version: e59506cc896acb7f7bf732d4fdf5e25f7ccd8983
- name: github.com/gorilla/sessions
version: 8b6b4cd75f07f7ee036eb37b8127bd40ab1efc49
version: a3acf13e802c358d65f249324d14ed24aac11370
- name: github.com/manucorporat/sse
version: ee05b128a739a0fb76c7ebd3ae4810c1de808d6d
- name: github.com/mattn/go-isatty
version: fc9e8d8ef48496124e79ae0df75490096eccf6fe
version: 6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c
- name: github.com/qiniu/api.v7
version: 9c12a67868f8f94d6a75dd6bb59b095db8d40d77
version: b7c7d6a2ce0aff8e5e7d14c39c3cde867efa1123
subpackages:
- api
- auth/qbox
- conf
- kodocli
- storage
- name: github.com/qiniu/x
version: f512abcf45ab4e2ba0fd4784c57b53d495997d66
subpackages:
- bytes.v7
- bytes.v7/seekable
- ctype.v7
- log.v7
- rpc.v7
- url.v7
- xlog.v7
- name: github.com/shurcooL/sanitized_anchor_name
version: 541ff5ee47f1dddf6a5281af78307d921524bcb5
version: 86672fcb3f950f35f2e675df2240550f2a50762f
- name: golang.org/x/crypto
version: dd85ac7e6a88fc6ca420478e934de5f1a42dd3c6
version: 13931e22f9e72ea58bb73048bc752b48c6d4d4ac
subpackages:
- acme
- acme/autocert
@@ -73,7 +70,7 @@ imports:
subpackages:
- context
- name: golang.org/x/sys
version: abf9c25f54453410d0c6668e519582a9e1115027
version: 810d7000345868fc619eb81f46307107118f4ae1
subpackages:
- unix
- name: gopkg.in/go-playground/validator.v8
@@ -86,19 +83,13 @@ imports:
- internal/sasl
- internal/scram
- name: gopkg.in/yaml.v2
version: 1be3d31502d6eabc0dd7ce5b0daab022e14a5538
- name: qiniupkg.com/api.v7
version: 9c12a67868f8f94d6a75dd6bb59b095db8d40d77
subpackages:
- kodo
- kodocli
version: d670f9405373e636a5a2765eea47fac0c9bc91a4
- name: qiniupkg.com/x
version: f512abcf45ab4e2ba0fd4784c57b53d495997d66
version: 946c4a16076d6d98aeb78619e2bd4012357f7228
subpackages:
- bytes.v7
- log.v7
- reqid.v7
- url.v7
testImports:
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
@@ -109,6 +100,6 @@ testImports:
subpackages:
- difflib
- name: github.com/stretchr/testify
version: f390dcf405f7b83c997eac1b06768bb9f44dec18
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
subpackages:
- assert

View File

@@ -1,5 +1,6 @@
package: github.com/eiblog/eiblog
import:
- package: github.com/deepzz0/logd
- package: github.com/eiblog/blackfriday
- package: github.com/eiblog/utils
subpackages:
@@ -7,19 +8,20 @@ import:
- mgo
- tmpl
- uuid
- package: github.com/gin-gonic/autotls
- package: github.com/gin-gonic/contrib
subpackages:
- sessions
- package: github.com/gin-gonic/gin
version: ~1.1.4
- package: github.com/qiniu/api.v7
subpackages:
- auth/qbox
- storage
- package: gopkg.in/mgo.v2
subpackages:
- bson
- package: gopkg.in/yaml.v2
- package: qiniupkg.com/api.v7
testImport:
- package: github.com/stretchr/testify
subpackages:
- kodo
- kodocli
- package: qiniupkg.com/x
subpackages:
- url.v7
- assert

View File

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

View File

@@ -44,15 +44,21 @@ func TestPickFirstImage(t *testing.T) {
}
func TestCovertStr(t *testing.T) {
now := time.Now().UTC()
testStr := []string{
time.Now().Format("2006-01-02T15:04:05"),
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"),
}
expectStr := []string{
JUST_NOW,
}
for i, v := range testStr {
assert.Equal(t, expectStr[i], ConvertStr(v))
time.Sleep(time.Second)
t.Log(now.Format("2006-01-02T15:04:05"))
for _, v := range testStr {
t.Log(v, ConvertStr(v))
}
}

View File

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

View File

@@ -18,7 +18,7 @@ func TestPingRPC(t *testing.T) {
}
pr.Params.Param = [4]rpcValue{
rpcValue{Value: Ei.BTitle},
rpcValue{Value: "https://" + setting.Conf.Mode.Domains[0]},
rpcValue{Value: "https://" + setting.Conf.Mode.Domain},
rpcValue{Value: "https://deepzz.com/post/gdb-debug.html"},
rpcValue{Value: "https://deepzz.com/rss.html"},
}

View File

@@ -7,94 +7,77 @@ import (
"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/auth/qbox"
"github.com/qiniu/api.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(nil, 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
@@ -103,9 +86,12 @@ func getKey(name string) string {
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

View File

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

View File

@@ -2,8 +2,9 @@
package main
import (
"crypto/rand"
"fmt"
"html/template"
"text/template"
"time"
"github.com/eiblog/eiblog/setting"
@@ -20,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: "/",
@@ -41,7 +49,7 @@ func init() {
}
return false
})
_, err := Tmpl.ParseFiles(files...)
_, err = Tmpl.ParseFiles(files...)
if err != nil {
logd.Fatal(err)
}
@@ -90,6 +98,7 @@ func init() {
}
}
// 开始运行
func Run() {
var (
endRunning = make(chan bool, 1)

View File

@@ -35,15 +35,16 @@ type Config struct {
Clean int // 清理回收箱频率
}
Disqus struct { // 获取文章数量相关
ShortName string
PublicKey string
AccessToken string
PostsCount string
PostsList string
PostCreate string
PostApprove string
Embed string
Interval int
ShortName string
PublicKey string
AccessToken string
PostsCount string
PostsList string
PostCreate string
PostApprove string
ThreadCreate string
Embed string
Interval int
}
Google struct { // 谷歌统计
URL string
@@ -51,8 +52,8 @@ type Config struct {
V string
T string
}
Kodo struct { // 七牛CDN
Name string
Qiniu struct { // 七牛CDN
Bucket string
Domain string
AccessKey string
SecretKey string

View File

@@ -67,10 +67,20 @@ type LogOption struct {
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, "/")
index := strings.LastIndex(wd, osSep())
logger := &Logger{
obj: wd[index+1:],
out: option.Out,

View File

@@ -9,6 +9,7 @@ go:
- 1.6
- 1.7
- 1.8
- 1.9
- tip
script:

View File

@@ -21,6 +21,7 @@ Documentation
- [API Reference](http://godoc.org/github.com/garyburd/redigo/redis)
- [FAQ](https://github.com/garyburd/redigo/wiki/FAQ)
- [Examples](https://godoc.org/github.com/garyburd/redigo/redis#pkg-examples)
Installation
------------

View File

@@ -29,9 +29,12 @@ import (
"time"
)
var (
_ ConnWithTimeout = (*conn)(nil)
)
// conn is the low-level implementation of Conn
type conn struct {
// Shared
mu sync.Mutex
pending int
@@ -73,10 +76,11 @@ type DialOption struct {
type dialOptions struct {
readTimeout time.Duration
writeTimeout time.Duration
dialer *net.Dialer
dial func(network, addr string) (net.Conn, error)
db int
password string
dialTLS bool
useTLS bool
skipVerify bool
tlsConfig *tls.Config
}
@@ -95,17 +99,27 @@ func DialWriteTimeout(d time.Duration) DialOption {
}}
}
// DialConnectTimeout specifies the timeout for connecting to the Redis server.
// DialConnectTimeout specifies the timeout for connecting to the Redis server when
// no DialNetDial option is specified.
func DialConnectTimeout(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
dialer := net.Dialer{Timeout: d}
do.dial = dialer.Dial
do.dialer.Timeout = d
}}
}
// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server
// when no DialNetDial option is specified.
// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then
// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected.
func DialKeepAlive(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
do.dialer.KeepAlive = d
}}
}
// DialNetDial specifies a custom dial function for creating TCP
// connections. If this option is left out, then net.Dial is
// used. DialNetDial overrides DialConnectTimeout.
// connections, otherwise a net.Dialer customized via the other options is used.
// DialNetDial overrides DialConnectTimeout and DialKeepAlive.
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
return DialOption{func(do *dialOptions) {
do.dial = dial
@@ -135,31 +149,49 @@ func DialTLSConfig(c *tls.Config) DialOption {
}}
}
// DialTLSSkipVerify to disable server name verification when connecting
// over TLS. Has no effect when not dialing a TLS connection.
// DialTLSSkipVerify disables server name verification when connecting over
// TLS. Has no effect when not dialing a TLS connection.
func DialTLSSkipVerify(skip bool) DialOption {
return DialOption{func(do *dialOptions) {
do.skipVerify = skip
}}
}
// DialUseTLS specifies whether TLS should be used when connecting to the
// server. This option is ignore by DialURL.
func DialUseTLS(useTLS bool) DialOption {
return DialOption{func(do *dialOptions) {
do.useTLS = useTLS
}}
}
// Dial connects to the Redis server at the given network and
// address using the specified options.
func Dial(network, address string, options ...DialOption) (Conn, error) {
do := dialOptions{
dial: net.Dial,
dialer: &net.Dialer{
KeepAlive: time.Minute * 5,
},
}
for _, option := range options {
option.f(&do)
}
if do.dial == nil {
do.dial = do.dialer.Dial
}
netConn, err := do.dial(network, address)
if err != nil {
return nil, err
}
if do.dialTLS {
tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify)
if do.useTLS {
var tlsConfig *tls.Config
if do.tlsConfig == nil {
tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify}
} else {
tlsConfig = cloneTLSConfig(do.tlsConfig)
}
if tlsConfig.ServerName == "" {
host, _, err := net.SplitHostPort(address)
if err != nil {
@@ -202,10 +234,6 @@ func Dial(network, address string, options ...DialOption) (Conn, error) {
return c, nil
}
func dialTLS(do *dialOptions) {
do.dialTLS = true
}
var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
// DialURL connects to a Redis server at the given URL using the Redis
@@ -257,9 +285,7 @@ func DialURL(rawurl string, options ...DialOption) (Conn, error) {
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
}
if u.Scheme == "rediss" {
options = append([]DialOption{{dialTLS}}, options...)
}
options = append(options, DialUseTLS(u.Scheme == "rediss"))
return Dial("tcp", address, options...)
}
@@ -344,43 +370,55 @@ func (c *conn) writeFloat64(n float64) error {
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
}
func (c *conn) writeCommand(cmd string, args []interface{}) (err error) {
func (c *conn) writeCommand(cmd string, args []interface{}) error {
c.writeLen('*', 1+len(args))
err = c.writeString(cmd)
if err := c.writeString(cmd); err != nil {
return err
}
for _, arg := range args {
if err != nil {
break
}
switch arg := arg.(type) {
case string:
err = c.writeString(arg)
case []byte:
err = c.writeBytes(arg)
case int:
err = c.writeInt64(int64(arg))
case int64:
err = c.writeInt64(arg)
case float64:
err = c.writeFloat64(arg)
case bool:
if arg {
err = c.writeString("1")
} else {
err = c.writeString("0")
}
case nil:
err = c.writeString("")
case Argument:
var buf bytes.Buffer
fmt.Fprint(&buf, arg.RedisArg())
err = c.writeBytes(buf.Bytes())
default:
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
err = c.writeBytes(buf.Bytes())
if err := c.writeArg(arg, true); err != nil {
return err
}
}
return err
return nil
}
func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) {
switch arg := arg.(type) {
case string:
return c.writeString(arg)
case []byte:
return c.writeBytes(arg)
case int:
return c.writeInt64(int64(arg))
case int64:
return c.writeInt64(arg)
case float64:
return c.writeFloat64(arg)
case bool:
if arg {
return c.writeString("1")
} else {
return c.writeString("0")
}
case nil:
return c.writeString("")
case Argument:
if argumentTypeOK {
return c.writeArg(arg.RedisArg(), false)
}
// See comment in default clause below.
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
return c.writeBytes(buf.Bytes())
default:
// This default clause is intended to handle builtin numeric types.
// The function should return an error for other types, but this is not
// done for compatibility with previous versions of the package.
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
return c.writeBytes(buf.Bytes())
}
}
type protocolError string
@@ -542,10 +580,17 @@ func (c *conn) Flush() error {
return nil
}
func (c *conn) Receive() (reply interface{}, err error) {
if c.readTimeout != 0 {
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
func (c *conn) Receive() (interface{}, error) {
return c.ReceiveWithTimeout(c.readTimeout)
}
func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
var deadline time.Time
if timeout != 0 {
deadline = time.Now().Add(timeout)
}
c.conn.SetReadDeadline(deadline)
if reply, err = c.readReply(); err != nil {
return nil, c.fatal(err)
}
@@ -568,6 +613,10 @@ func (c *conn) Receive() (reply interface{}, err error) {
}
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
return c.DoWithTimeout(c.readTimeout, cmd, args...)
}
func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
c.mu.Lock()
pending := c.pending
c.pending = 0
@@ -591,9 +640,11 @@ func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
return nil, c.fatal(err)
}
if c.readTimeout != 0 {
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
var deadline time.Time
if readTimeout != 0 {
deadline = time.Now().Add(readTimeout)
}
c.conn.SetReadDeadline(deadline)
if cmd == "" {
reply := make([]interface{}, pending)

View File

@@ -16,6 +16,9 @@ package redis_test
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"math"
"net"
@@ -31,18 +34,45 @@ import (
type testConn struct {
io.Reader
io.Writer
readDeadline time.Time
writeDeadline time.Time
}
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 (*testConn) Close() error { return nil }
func (*testConn) LocalAddr() net.Addr { return nil }
func (*testConn) RemoteAddr() net.Addr { return nil }
func (c *testConn) SetDeadline(t time.Time) error { c.readDeadline = t; c.writeDeadline = t; return nil }
func (c *testConn) SetReadDeadline(t time.Time) error { c.readDeadline = t; return nil }
func (c *testConn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t; 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
func dialTestConn(r string, w io.Writer) redis.DialOption {
return redis.DialNetDial(func(network, addr string) (net.Conn, error) {
return &testConn{Reader: strings.NewReader(r), Writer: w}, nil
})
}
type tlsTestConn struct {
net.Conn
done chan struct{}
}
func (c *tlsTestConn) Close() error {
c.Conn.Close()
<-c.done
return nil
}
func dialTestConnTLS(r string, w io.Writer) redis.DialOption {
return redis.DialNetDial(func(network, addr string) (net.Conn, error) {
client, server := net.Pipe()
tlsServer := tls.Server(server, &serverTLSConfig)
go io.Copy(tlsServer, strings.NewReader(r))
done := make(chan struct{})
go func() {
io.Copy(w, tlsServer)
close(done)
}()
return &tlsTestConn{Conn: client, done: done}, nil
})
}
@@ -54,6 +84,10 @@ func (t durationArg) RedisArg() interface{} {
return t.Seconds()
}
type recursiveArg int
func (v recursiveArg) RedisArg() interface{} { return v }
var writeTests = []struct {
args []interface{}
expected string
@@ -94,6 +128,10 @@ var writeTests = []struct {
[]interface{}{"SET", "key", durationArg{time.Minute}},
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$2\r\n60\r\n",
},
{
[]interface{}{"SET", "key", recursiveArg(123)},
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n123\r\n",
},
{
[]interface{}{"ECHO", true, false},
"*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n",
@@ -103,7 +141,7 @@ var writeTests = []struct {
func TestWrite(t *testing.T) {
for _, tt := range writeTests {
var buf bytes.Buffer
c, _ := redis.Dial("", "", dialTestConn(nil, &buf))
c, _ := redis.Dial("", "", dialTestConn("", &buf))
err := c.Send(tt.args[0].(string), tt.args[1:]...)
if err != nil {
t.Errorf("Send(%v) returned error %v", tt.args, err)
@@ -202,7 +240,7 @@ var readTests = []struct {
func TestRead(t *testing.T) {
for _, tt := range readTests {
c, _ := redis.Dial("", "", dialTestConn(strings.NewReader(tt.reply), nil))
c, _ := redis.Dial("", "", dialTestConn(tt.reply, nil))
actual, err := c.Receive()
if tt.expected == errorSentinel {
if err == nil {
@@ -514,41 +552,85 @@ func TestDialURLHost(t *testing.T) {
}
}
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)
var dialURLTests = []struct {
description string
url string
r string
w string
}{
{"password", "redis://x:abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"},
{"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"},
{"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"},
{"no database", "redis://localhost/", "+OK\r\n", ""},
}
func TestDialURL(t *testing.T) {
for _, tt := range dialURLTests {
var buf bytes.Buffer
// UseTLS should be ignored in all of these tests.
_, err := redis.DialURL(tt.url, dialTestConn(tt.r, &buf), redis.DialUseTLS(true))
if err != nil {
t.Errorf("%s dial error: %v", tt.description, err)
continue
}
if w := buf.String(); w != tt.w {
t.Errorf("%s commands = %q, want %q", tt.description, w, tt.w)
}
}
expected := "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"
}
func checkPingPong(t *testing.T, buf *bytes.Buffer, c redis.Conn) {
resp, err := c.Do("PING")
if err != nil {
t.Fatal("ping error:", err)
}
// Close connection to ensure that writes to buf are complete.
c.Close()
expected := "*1\r\n$4\r\nPING\r\n"
actual := buf.String()
if actual != expected {
t.Errorf("commands = %q, want %q", actual, expected)
}
if resp != "PONG" {
t.Errorf("resp = %v, want %v", resp, "PONG")
}
}
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)
const pingResponse = "+PONG\r\n"
func TestDialURLTLS(t *testing.T) {
var buf bytes.Buffer
c, err := redis.DialURL("rediss://example.com/",
redis.DialTLSConfig(&clientTLSConfig),
dialTestConnTLS(pingResponse, &buf))
if err != nil {
t.Fatal("dial error:", err)
}
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)
checkPingPong(t, &buf, c)
}
func TestDialUseTLS(t *testing.T) {
var buf bytes.Buffer
c, err := redis.Dial("tcp", "example.com:6379",
redis.DialTLSConfig(&clientTLSConfig),
dialTestConnTLS(pingResponse, &buf),
redis.DialUseTLS(true))
if err != nil {
t.Fatal("dial error:", err)
}
// 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)
checkPingPong(t, &buf, c)
}
func TestDialTLSSKipVerify(t *testing.T) {
var buf bytes.Buffer
c, err := redis.Dial("tcp", "example.com:6379",
dialTestConnTLS(pingResponse, &buf),
redis.DialTLSSkipVerify(true),
redis.DialUseTLS(true))
if err != nil {
t.Fatal("dial error:", err)
}
checkPingPong(t, &buf, c)
}
// Connect to local instance of Redis running on the default port.
@@ -680,3 +762,106 @@ func BenchmarkDoPing(b *testing.B) {
}
}
}
var clientTLSConfig, serverTLSConfig tls.Config
func init() {
// The certificate and key for testing TLS dial options was created
// using the command
//
// go run GOROOT/src/crypto/tls/generate_cert.go \
// --rsa-bits 1024 \
// --host 127.0.0.1,::1,example.com --ca \
// --start-date "Jan 1 00:00:00 1970" \
// --duration=1000000h
//
// where GOROOT is the value of GOROOT reported by go env.
localhostCert := []byte(`
-----BEGIN CERTIFICATE-----
MIICFDCCAX2gAwIBAgIRAJfBL4CUxkXcdlFurb3K+iowDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
gYkCgYEArizw8WxMUQ3bGHLeuJ4fDrEpy+L2pqrbYRlKk1DasJ/VkB8bImzIpe6+
LGjiYIxvnDCOJ3f3QplcQuiuMyl6f2irJlJsbFT8Lo/3obnuTKAIaqUdJUqBg6y+
JaL8Auk97FvunfKFv8U1AIhgiLzAfQ/3Eaq1yi87Ra6pMjGbTtcCAwEAAaNoMGYw
DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAdZ8daIVkyhVwflt5I19m0oq1TycbGO1+
ach7T6cZiBQeNR/SJtxr/wKPEpmvUgbv2BfFrKJ8QoIHYsbNSURTWSEa02pfw4k9
6RQhij3ZkG79Ituj5OYRORV6Z0HUW32r670BtcuHuAhq7YA6Nxy4FtSt7bAlVdRt
rrKgNsltzMk=
-----END CERTIFICATE-----`)
localhostKey := []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCuLPDxbExRDdsYct64nh8OsSnL4vamqtthGUqTUNqwn9WQHxsi
bMil7r4saOJgjG+cMI4nd/dCmVxC6K4zKXp/aKsmUmxsVPwuj/ehue5MoAhqpR0l
SoGDrL4lovwC6T3sW+6d8oW/xTUAiGCIvMB9D/cRqrXKLztFrqkyMZtO1wIDAQAB
AoGACrc5G6FOEK6JjDeE/Fa+EmlT6PdNtXNNi+vCas3Opo8u1G8VfEi1D4BgstrB
Eq+RLkrOdB8tVyuYQYWPMhabMqF+hhKJN72j0OwfuPlVvTInwb/cKjo/zbH1IA+Y
HenHNK4ywv7/p/9/MvQPJ3I32cQBCgGUW5chVSH5M1sj5gECQQDabQAI1X0uDqCm
KbX9gXVkAgxkFddrt6LBHt57xujFcqEKFE7nwKhDh7DweVs/VEJ+kpid4z+UnLOw
KjtP9JolAkEAzCNBphQ//IsbH5rNs10wIUw3Ks/Oepicvr6kUFbIv+neRzi1iJHa
m6H7EayK3PWgax6BAsR/t0Jc9XV7r2muSwJAVzN09BHnK+ADGtNEKLTqXMbEk6B0
pDhn7ZmZUOkUPN+Kky+QYM11X6Bob1jDqQDGmymDbGUxGO+GfSofC8inUQJAGfci
Eo3g1a6b9JksMPRZeuLG4ZstGErxJRH6tH1Va5PDwitka8qhk8o2tTjNMO3NSdLH
diKoXBcE2/Pll5pJoQJBAIMiiMIzXJhnN4mX8may44J/HvMlMf2xuVH2gNMwmZuc
Bjqn3yoLHaoZVvbWOi0C2TCN4FjXjaLNZGifQPbIcaA=
-----END RSA PRIVATE KEY-----`)
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
if err != nil {
panic(fmt.Sprintf("error creating key pair: %v", err))
}
serverTLSConfig.Certificates = []tls.Certificate{cert}
certificate, err := x509.ParseCertificate(serverTLSConfig.Certificates[0].Certificate[0])
if err != nil {
panic(fmt.Sprintf("error parsing x509 certificate: %v", err))
}
clientTLSConfig.RootCAs = x509.NewCertPool()
clientTLSConfig.RootCAs.AddCert(certificate)
}
func TestWithTimeout(t *testing.T) {
for _, recv := range []bool{true, false} {
for _, defaultTimout := range []time.Duration{0, time.Minute} {
var buf bytes.Buffer
nc := &testConn{Reader: strings.NewReader("+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n"), Writer: &buf}
c, _ := redis.Dial("", "", redis.DialReadTimeout(defaultTimout), redis.DialNetDial(func(network, addr string) (net.Conn, error) { return nc, nil }))
for i := 0; i < 4; i++ {
var minDeadline, maxDeadline time.Time
// Alternate between default and specified timeout.
if i%2 == 0 {
if defaultTimout != 0 {
minDeadline = time.Now().Add(defaultTimout)
}
if recv {
c.Receive()
} else {
c.Do("PING")
}
if defaultTimout != 0 {
maxDeadline = time.Now().Add(defaultTimout)
}
} else {
timeout := 10 * time.Minute
minDeadline = time.Now().Add(timeout)
if recv {
redis.ReceiveWithTimeout(c, timeout)
} else {
redis.DoWithTimeout(c, timeout, "PING")
}
maxDeadline = time.Now().Add(timeout)
}
// Expect set deadline in expected range.
if nc.readDeadline.Before(minDeadline) || nc.readDeadline.After(maxDeadline) {
t.Errorf("recv %v, %d: do deadline error: %v, %v, %v", recv, i, minDeadline, nc.readDeadline, maxDeadline)
}
}
}
}
}

View File

@@ -38,7 +38,7 @@
//
// n, err := conn.Do("APPEND", "key", "value")
//
// The Do method converts command arguments to binary strings for transmission
// The Do method converts command arguments to bulk strings for transmission
// to the server as follows:
//
// Go Type Conversion
@@ -48,7 +48,7 @@
// float64 strconv.FormatFloat(v, 'g', -1, 64)
// bool true -> "1", false -> "0"
// nil ""
// all other types fmt.Print(v)
// all other types fmt.Fprint(w, v)
//
// Redis command reply types are represented using the following Go types:
//

View File

@@ -4,11 +4,7 @@ package redis
import "crypto/tls"
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
if cfg == nil {
return &tls.Config{InsecureSkipVerify: skipVerify}
}
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,

View File

@@ -1,14 +1,10 @@
// +build go1.7
// +build go1.7,!go1.8
package redis
import "crypto/tls"
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
if cfg == nil {
return &tls.Config{InsecureSkipVerify: skipVerify}
}
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,

9
vendor/github.com/garyburd/redigo/redis/go18.go generated vendored Normal file
View File

@@ -0,0 +1,9 @@
// +build go1.8
package redis
import "crypto/tls"
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
return cfg.Clone()
}

View File

@@ -18,6 +18,11 @@ import (
"bytes"
"fmt"
"log"
"time"
)
var (
_ ConnWithTimeout = (*loggingConn)(nil)
)
// NewLoggingConn returns a logging wrapper around a connection.
@@ -104,6 +109,12 @@ func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{},
return reply, err
}
func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) {
reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...)
c.print("DoWithTimeout", commandName, args, reply, err)
return reply, err
}
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
err := c.Conn.Send(commandName, args...)
c.print("Send", commandName, args, nil, err)
@@ -115,3 +126,9 @@ func (c *loggingConn) Receive() (interface{}, error) {
c.print("Receive", "", nil, reply, err)
return reply, err
}
func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
reply, err := ReceiveWithTimeout(c.Conn, timeout)
c.print("ReceiveWithTimeout", "", nil, reply, err)
return reply, err
}

View File

@@ -28,6 +28,11 @@ import (
"github.com/garyburd/redigo/internal"
)
var (
_ ConnWithTimeout = (*pooledConnection)(nil)
_ ConnWithTimeout = (*errorConnection)(nil)
)
var nowFunc = time.Now // for testing
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
@@ -96,7 +101,7 @@ var (
// return nil, err
// }
// return c, nil
// }
// },
// }
//
// Use the TestOnBorrow function to check the health of an idle connection
@@ -115,7 +120,6 @@ var (
// }
//
type Pool struct {
// Dial is an application supplied function for creating and configuring a
// connection.
//
@@ -181,6 +185,26 @@ func (p *Pool) Get() Conn {
return &pooledConnection{p: p, c: c}
}
// PoolStats contains pool statistics.
type PoolStats struct {
// ActiveCount is the number of connections in the pool. The count includes idle connections and connections in use.
ActiveCount int
// IdleCount is the number of idle connections in the pool.
IdleCount int
}
// Stats returns pool's statistics.
func (p *Pool) Stats() PoolStats {
p.mu.Lock()
stats := PoolStats{
ActiveCount: p.active,
IdleCount: p.idle.Len(),
}
p.mu.Unlock()
return stats
}
// ActiveCount returns the number of connections in the pool. The count includes idle connections and connections in use.
func (p *Pool) ActiveCount() int {
p.mu.Lock()
@@ -249,7 +273,6 @@ func (p *Pool) get() (Conn, error) {
}
for {
// Get idle connection.
for i, n := 0, p.idle.Len(); i < n; i++ {
@@ -400,6 +423,16 @@ func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply i
return pc.c.Do(commandName, args...)
}
func (pc *pooledConnection) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) {
cwt, ok := pc.c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
return cwt.DoWithTimeout(timeout, commandName, args...)
}
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
@@ -414,11 +447,23 @@ func (pc *pooledConnection) Receive() (reply interface{}, err error) {
return pc.c.Receive()
}
func (pc *pooledConnection) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
cwt, ok := pc.c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
return cwt.ReceiveWithTimeout(timeout)
}
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 }
func (ec errorConnection) DoWithTimeout(time.Duration, 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 nil }
func (ec errorConnection) Flush() error { return ec.err }
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }
func (ec errorConnection) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err }

View File

@@ -92,12 +92,15 @@ func (d *poolDialer) check(message string, p *redis.Pool, dialed, open, inuse in
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)
stats := p.Stats()
if stats.ActiveCount != open {
d.t.Errorf("%s: active=%d, want %d", message, stats.ActiveCount, open)
}
if idle := p.IdleCount(); idle != open-inuse {
d.t.Errorf("%s: idle=%d, want %d", message, idle, open-inuse)
if stats.IdleCount != open-inuse {
d.t.Errorf("%s: idle=%d, want %d", message, stats.IdleCount, open-inuse)
}
d.mu.Unlock()
}

View File

@@ -14,11 +14,13 @@
package redis
import "errors"
import (
"errors"
"time"
)
// Subscription represents a subscribe or unsubscribe notification.
type Subscription struct {
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
Kind string
@@ -31,7 +33,6 @@ type Subscription struct {
// Message represents a message notification.
type Message struct {
// The originating channel.
Channel string
@@ -41,7 +42,6 @@ type Message struct {
// PMessage represents a pmessage notification.
type PMessage struct {
// The matched pattern.
Pattern string
@@ -94,6 +94,9 @@ func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
}
// Ping sends a PING to the server with the specified data.
//
// The connection must be subscribed to at least one channel or pattern when
// calling this method.
func (c PubSubConn) Ping(data string) error {
c.Conn.Send("PING", data)
return c.Conn.Flush()
@@ -103,7 +106,17 @@ func (c PubSubConn) Ping(data string) error {
// or error. The return value is intended to be used directly in a type switch
// as illustrated in the PubSubConn example.
func (c PubSubConn) Receive() interface{} {
reply, err := Values(c.Conn.Receive())
return c.receiveInternal(c.Conn.Receive())
}
// ReceiveWithTimeout is like Receive, but it allows the application to
// override the connection's default timeout.
func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} {
return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout))
}
func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} {
reply, err := Values(replyArg, errArg)
if err != nil {
return err
}

View File

@@ -0,0 +1,165 @@
// 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.
// +build go1.7
package redis_test
import (
"context"
"fmt"
"time"
"github.com/garyburd/redigo/redis"
)
// listenPubSubChannels listens for messages on Redis pubsub channels. The
// onStart function is called after the channels are subscribed. The onMessage
// function is called for each message.
func listenPubSubChannels(ctx context.Context, redisServerAddr string,
onStart func() error,
onMessage func(channel string, data []byte) error,
channels ...string) error {
// A ping is set to the server with this period to test for the health of
// the connection and server.
const healthCheckPeriod = time.Minute
c, err := redis.Dial("tcp", redisServerAddr,
// Read timeout on server should be greater than ping period.
redis.DialReadTimeout(healthCheckPeriod+10*time.Second),
redis.DialWriteTimeout(10*time.Second))
if err != nil {
return err
}
defer c.Close()
psc := redis.PubSubConn{Conn: c}
if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil {
return err
}
done := make(chan error, 1)
// Start a goroutine to receive notifications from the server.
go func() {
for {
switch n := psc.Receive().(type) {
case error:
done <- n
return
case redis.Message:
if err := onMessage(n.Channel, n.Data); err != nil {
done <- err
return
}
case redis.Subscription:
switch n.Count {
case len(channels):
// Notify application when all channels are subscribed.
if err := onStart(); err != nil {
done <- err
return
}
case 0:
// Return from the goroutine when all channels are unsubscribed.
done <- nil
return
}
}
}
}()
ticker := time.NewTicker(healthCheckPeriod)
defer ticker.Stop()
loop:
for err == nil {
select {
case <-ticker.C:
// Send ping to test health of connection and server. If
// corresponding pong is not received, then receive on the
// connection will timeout and the receive goroutine will exit.
if err = psc.Ping(""); err != nil {
break loop
}
case <-ctx.Done():
break loop
case err := <-done:
// Return error from the receive goroutine.
return err
}
}
// Signal the receiving goroutine to exit by unsubscribing from all channels.
psc.Unsubscribe()
// Wait for goroutine to complete.
return <-done
}
func publish() {
c, err := dial()
if err != nil {
fmt.Println(err)
return
}
defer c.Close()
c.Do("PUBLISH", "c1", "hello")
c.Do("PUBLISH", "c2", "world")
c.Do("PUBLISH", "c1", "goodbye")
}
// This example shows how receive pubsub notifications with cancelation and
// health checks.
func ExamplePubSubConn() {
redisServerAddr, err := serverAddr()
if err != nil {
fmt.Println(err)
return
}
ctx, cancel := context.WithCancel(context.Background())
err = listenPubSubChannels(ctx,
redisServerAddr,
func() error {
// The start callback is a good place to backfill missed
// notifications. For the purpose of this example, a goroutine is
// started to send notifications.
go publish()
return nil
},
func(channel string, message []byte) error {
fmt.Printf("channel: %s, message: %s\n", channel, message)
// For the purpose of this example, cancel the listener's context
// after receiving last message sent by publish().
if string(message) == "goodbye" {
cancel()
}
return nil
},
"c1", "c2")
if err != nil {
fmt.Println(err)
return
}
// Output:
// channel: c1, message: hello
// channel: c2, message: world
// channel: c1, message: goodbye
}

View File

@@ -15,93 +15,13 @@
package redis_test
import (
"fmt"
"reflect"
"sync"
"testing"
"time"
"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) {
@@ -145,4 +65,10 @@ func TestPushed(t *testing.T) {
c.Conn.Send("PING")
c.Conn.Flush()
expectPushed(t, c, `Send("PING")`, redis.Pong{})
c.Ping("timeout")
got := c.ReceiveWithTimeout(time.Minute)
if want := (redis.Pong{Data: "timeout"}); want != got {
t.Errorf("recv /w timeout got %v, want %v", got, want)
}
}

View File

@@ -14,6 +14,11 @@
package redis
import (
"errors"
"time"
)
// Error represents an error returned in a command reply.
type Error string
@@ -40,20 +45,73 @@ type Conn interface {
Receive() (reply interface{}, err error)
}
// Argument is implemented by types which want to control how their value is
// interpreted when used as an argument to a redis command.
// Argument is the interface implemented by an object which wants to control how
// the object is converted to Redis bulk strings.
type Argument interface {
// RedisArg returns the interface that represents the value to be used
// in redis commands.
// RedisArg returns a value to be encoded as a bulk string per the
// conversions listed in the section 'Executing Commands'.
// Implementations should typically return a []byte or string.
RedisArg() interface{}
}
// Scanner is implemented by types which want to control how their value is
// interpreted when read from redis.
// Scanner is implemented by an object which wants to control its value is
// interpreted when read from Redis.
type Scanner interface {
// RedisScan assigns a value from a redis value.
// RedisScan assigns a value from a Redis value. The argument src is one of
// the reply types listed in the section `Executing Commands`.
//
// An error should be returned if the value cannot be stored without
// loss of information.
RedisScan(src interface{}) error
}
// ConnWithTimeout is an optional interface that allows the caller to override
// a connection's default read timeout. This interface is useful for executing
// the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the
// server.
//
// A connection's default read timeout is set with the DialReadTimeout dial
// option. Applications should rely on the default timeout for commands that do
// not block at the server.
//
// All of the Conn implementations in this package satisfy the ConnWithTimeout
// interface.
//
// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify
// use of this interface.
type ConnWithTimeout interface {
Conn
// Do sends a command to the server and returns the received reply.
// The timeout overrides the read timeout set when dialing the
// connection.
DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error)
// Receive receives a single reply from the Redis server. The timeout
// overrides the read timeout set when dialing the connection.
ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)
}
var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout")
// DoWithTimeout executes a Redis command with the specified read timeout. If
// the connection does not satisfy the ConnWithTimeout interface, then an error
// is returned.
func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
cwt, ok := c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
return cwt.DoWithTimeout(timeout, cmd, args...)
}
// ReceiveWithTimeout receives a reply with the specified read timeout. If the
// connection does not satisfy the ConnWithTimeout interface, then an error is
// returned.
func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) {
cwt, ok := c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
return cwt.ReceiveWithTimeout(timeout)
}

71
vendor/github.com/garyburd/redigo/redis/redis_test.go generated vendored Normal file
View File

@@ -0,0 +1,71 @@
// Copyright 2017 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 (
"testing"
"time"
"github.com/garyburd/redigo/redis"
)
type timeoutTestConn int
func (tc timeoutTestConn) Do(string, ...interface{}) (interface{}, error) {
return time.Duration(-1), nil
}
func (tc timeoutTestConn) DoWithTimeout(timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
return timeout, nil
}
func (tc timeoutTestConn) Receive() (interface{}, error) {
return time.Duration(-1), nil
}
func (tc timeoutTestConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
return timeout, nil
}
func (tc timeoutTestConn) Send(string, ...interface{}) error { return nil }
func (tc timeoutTestConn) Err() error { return nil }
func (tc timeoutTestConn) Close() error { return nil }
func (tc timeoutTestConn) Flush() error { return nil }
func testTimeout(t *testing.T, c redis.Conn) {
r, err := c.Do("PING")
if r != time.Duration(-1) || err != nil {
t.Errorf("Do() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil)
}
r, err = redis.DoWithTimeout(c, time.Minute, "PING")
if r != time.Minute || err != nil {
t.Errorf("DoWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil)
}
r, err = c.Receive()
if r != time.Duration(-1) || err != nil {
t.Errorf("Receive() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil)
}
r, err = redis.ReceiveWithTimeout(c, time.Minute)
if r != time.Minute || err != nil {
t.Errorf("ReceiveWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil)
}
}
func TestConnTimeout(t *testing.T) {
testTimeout(t, timeoutTestConn(0))
}
func TestPoolConnTimeout(t *testing.T) {
p := &redis.Pool{Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil }}
testTimeout(t, p.Get())
}

View File

@@ -243,34 +243,67 @@ func Values(reply interface{}, err error) ([]interface{}, error) {
return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
}
func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error {
if err != nil {
return err
}
switch reply := reply.(type) {
case []interface{}:
makeSlice(len(reply))
for i := range reply {
if reply[i] == nil {
continue
}
if err := assign(i, reply[i]); err != nil {
return err
}
}
return nil
case nil:
return ErrNil
case Error:
return reply
}
return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply)
}
// Float64s is a helper that converts an array command reply to a []float64. If
// err is not equal to nil, then Float64s returns nil, err. Nil array items are
// converted to 0 in the output slice. Floats64 returns an error if an array
// item is not a bulk string or nil.
func Float64s(reply interface{}, err error) ([]float64, error) {
var result []float64
err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error {
p, ok := v.([]byte)
if !ok {
return fmt.Errorf("redigo: unexpected element type for Floats64, got type %T", v)
}
f, err := strconv.ParseFloat(string(p), 64)
result[i] = f
return err
})
return result, err
}
// Strings is a helper that converts an array command reply to a []string. If
// err is not equal to nil, then Strings returns nil, err. Nil array items are
// converted to "" in the output slice. Strings returns an error if an array
// item is not a bulk string or nil.
func Strings(reply interface{}, err error) ([]string, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []interface{}:
result := make([]string, len(reply))
for i := range reply {
if reply[i] == nil {
continue
}
p, ok := reply[i].([]byte)
if !ok {
return nil, fmt.Errorf("redigo: unexpected element type for Strings, got type %T", reply[i])
}
result[i] = string(p)
var result []string
err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error {
switch v := v.(type) {
case string:
result[i] = v
return nil
case []byte:
result[i] = string(v)
return nil
default:
return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v)
}
return result, nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("redigo: unexpected type for Strings, got type %T", reply)
})
return result, err
}
// ByteSlices is a helper that converts an array command reply to a [][]byte.
@@ -278,43 +311,64 @@ func Strings(reply interface{}, err error) ([]string, error) {
// items are stay nil. ByteSlices returns an error if an array item is not a
// bulk string or nil.
func ByteSlices(reply interface{}, err error) ([][]byte, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []interface{}:
result := make([][]byte, len(reply))
for i := range reply {
if reply[i] == nil {
continue
}
p, ok := reply[i].([]byte)
if !ok {
return nil, fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", reply[i])
}
result[i] = p
var result [][]byte
err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error {
p, ok := v.([]byte)
if !ok {
return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v)
}
return result, nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("redigo: unexpected type for ByteSlices, got type %T", reply)
result[i] = p
return nil
})
return result, err
}
// Ints is a helper that converts an array command reply to a []int. If
// err is not equal to nil, then Ints returns nil, err.
// Int64s is a helper that converts an array command reply to a []int64.
// If err is not equal to nil, then Int64s returns nil, err. Nil array
// items are stay nil. Int64s returns an error if an array item is not a
// bulk string or nil.
func Int64s(reply interface{}, err error) ([]int64, error) {
var result []int64
err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error {
switch v := v.(type) {
case int64:
result[i] = v
return nil
case []byte:
n, err := strconv.ParseInt(string(v), 10, 64)
result[i] = n
return err
default:
return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v)
}
})
return result, err
}
// Ints is a helper that converts an array command reply to a []in.
// If err is not equal to nil, then Ints returns nil, err. Nil array
// items are stay nil. Ints returns an error if an array item is not a
// bulk string or nil.
func Ints(reply interface{}, err error) ([]int, error) {
var ints []int
values, err := Values(reply, err)
if err != nil {
return ints, err
}
if err := ScanSlice(values, &ints); err != nil {
return ints, err
}
return ints, nil
var result []int
err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error {
switch v := v.(type) {
case int64:
n := int(v)
if int64(n) != v {
return strconv.ErrRange
}
result[i] = n
return nil
case []byte:
n, err := strconv.Atoi(string(v))
result[i] = n
return err
default:
return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v)
}
})
return result, err
}
// StringMap is a helper that converts an array of strings (alternating key, value)

View File

@@ -37,24 +37,44 @@ var replyTests = []struct {
expected valueError
}{
{
"ints([v1, v2])",
"ints([[]byte, []byte])",
ve(redis.Ints([]interface{}{[]byte("4"), []byte("5")}, nil)),
ve([]int{4, 5}, nil),
},
{
"ints([nt64, int64])",
ve(redis.Ints([]interface{}{int64(4), int64(5)}, nil)),
ve([]int{4, 5}, nil),
},
{
"ints([[]byte, nil, []byte])",
ve(redis.Ints([]interface{}{[]byte("4"), nil, []byte("5")}, nil)),
ve([]int{4, 0, 5}, nil),
},
{
"ints(nil)",
ve(redis.Ints(nil, nil)),
ve([]int(nil), redis.ErrNil),
},
{
"strings([v1, v2])",
"int64s([[]byte, []byte])",
ve(redis.Int64s([]interface{}{[]byte("4"), []byte("5")}, nil)),
ve([]int64{4, 5}, nil),
},
{
"int64s([int64, int64])",
ve(redis.Int64s([]interface{}{int64(4), int64(5)}, nil)),
ve([]int64{4, 5}, nil),
},
{
"strings([[]byte, []bytev2])",
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),
"strings([string, string])",
ve(redis.Strings([]interface{}{"v1", "v2"}, nil)),
ve([]string{"v1", "v2"}, nil),
},
{
"byteslices([v1, v2])",
@@ -62,9 +82,9 @@ var replyTests = []struct {
ve([][]byte{[]byte("v1"), []byte("v2")}, nil),
},
{
"byteslices(nil)",
ve(redis.ByteSlices(nil, nil)),
ve([][]byte(nil), redis.ErrNil),
"float64s([v1, v2])",
ve(redis.Float64s([]interface{}{[]byte("1.234"), []byte("5.678")}, nil)),
ve([]float64{1.234, 5.678}, nil),
},
{
"values([v1, v2])",
@@ -120,6 +140,11 @@ func dial() (redis.Conn, error) {
return redis.DialDefaultServer()
}
// serverAddr wraps DefaultServerAddr() with a more suitable function name for examples.
func serverAddr() (string, error) {
return redis.DefaultServerAddr()
}
func ExampleBool() {
c, err := dial()
if err != nil {

View File

@@ -38,6 +38,7 @@ var (
ErrNegativeInt = errNegativeInt
serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary")
serverAddress = flag.String("redis-address", "127.0.0.1", "The address of the server")
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
@@ -96,7 +97,8 @@ func (s *Server) watch(r io.Reader, ready chan error) {
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") {
if strings.Contains(text, " * Ready to accept connections") ||
strings.Contains(text, " * The server is now ready to accept connections on port") {
listening = true
ready <- nil
}
@@ -125,28 +127,32 @@ func stopDefaultServer() {
}
}
// startDefaultServer starts the default server if not already running.
func startDefaultServer() error {
// DefaultServerAddr starts the test server if not already started and returns
// the address of that server.
func DefaultServerAddr() (string, error) {
defaultServerMu.Lock()
defer defaultServerMu.Unlock()
addr := fmt.Sprintf("%v:%d", *serverAddress, *serverBasePort)
if defaultServer != nil || defaultServerErr != nil {
return defaultServerErr
return addr, defaultServerErr
}
defaultServer, defaultServerErr = NewServer(
"default",
"--port", strconv.Itoa(*serverBasePort),
"--bind", *serverAddress,
"--save", "",
"--appendonly", "no")
return defaultServerErr
return addr, 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 {
addr, err := DefaultServerAddr()
if err != nil {
return nil, err
}
c, err := Dial("tcp", fmt.Sprintf(":%d", *serverBasePort), DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second))
c, err := Dial("tcp", addr, DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second))
if err != nil {
return nil, err
}

View File

@@ -4,6 +4,7 @@ go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- master
git:

View File

@@ -1,4 +1,4 @@
// Support Let's Encrypt for a Go server application.
// Package autotls support Let's Encrypt for a Go server application.
//
// package main
//

View File

@@ -34,3 +34,5 @@ Each author is responsible of maintaining his own code, although if you submit a
+ [gin-oauth2](https://github.com/zalando/gin-oauth2) - for working with OAuth2
+ [static](https://github.com/hyperboloide/static) An alternative static assets handler for the gin framework.
+ [xss-mw](https://github.com/dvwright/xss-mw) - XssMw is a middleware designed to "auto remove XSS" from user submitted input
+ [gin-helmet](https://github.com/danielkov/gin-helmet) - Collection of simple security middleware.
+ [gin-jwt-session](https://github.com/ScottHuangZL/gin-jwt-session) - middleware to provide JWT/Session/Flashes, easy to use while also provide options for adjust if necessary. Provide sample too.

View File

@@ -5,9 +5,9 @@ import (
"os"
"time"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/contrib/ginrus"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func main() {

View File

@@ -6,10 +6,14 @@ package ginrus
import (
"time"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
type loggerEntryWithFields interface {
WithFields(fields logrus.Fields) *logrus.Entry
}
// Ginrus returns a gin.HandlerFunc (middleware) that logs requests using logrus.
//
// Requests with errors are logged using logrus.Error().
@@ -18,7 +22,7 @@ import (
// It receives:
// 1. A time package format string (e.g. time.RFC3339).
// 2. A boolean stating whether to use UTC time zone or local.
func Ginrus(logger *logrus.Logger, timeFormat string, utc bool) gin.HandlerFunc {
func Ginrus(logger loggerEntryWithFields, timeFormat string, utc bool) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// some evil middlewares modify this values

View File

@@ -44,14 +44,14 @@ Let's start with an example that shows the sessions API in a nutshell:
First we initialize a session store calling `NewCookieStore()` and passing a
secret key used to authenticate the session. Inside the handler, we call
`store.Get()` to retrieve an existing session or a new one. Then we set some
session values in session.Values, which is a `map[interface{}]interface{}`.
`store.Get()` to retrieve an existing session or create a new one. Then we set
some session values in session.Values, which is a `map[interface{}]interface{}`.
And finally we call `session.Save()` to save the session in the response.
Important Note: If you aren't using gorilla/mux, you need to wrap your handlers
with
[`context.ClearHandler`](http://www.gorillatoolkit.org/pkg/context#ClearHandler)
as or else you will leak memory! An easy way to do this is to wrap the top-level
or else you will leak memory! An easy way to do this is to wrap the top-level
mux when calling http.ListenAndServe:
```go

View File

@@ -59,6 +59,10 @@ func TestGH2MaxLength(t *testing.T) {
w := httptest.NewRecorder()
session, err := store.New(req, "my session")
if err != nil {
t.Fatal("failed to create session", err)
}
session.Values["big"] = make([]byte, base64.StdEncoding.DecodedLen(4096*2))
err = session.Save(req, w)
if err == nil {

View File

@@ -2,6 +2,10 @@ language: go
go:
- tip
os:
- linux
- osx
before_install:
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover

View File

@@ -1,5 +1,5 @@
// +build linux
// +build !appengine
// +build !appengine,!ppc64,!ppc64le
package isatty

View File

@@ -0,0 +1,19 @@
// +build linux
// +build ppc64 ppc64le
package isatty
import (
"unsafe"
syscall "golang.org/x/sys/unix"
)
const ioctlReadTermios = syscall.TCGETS
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

View File

@@ -3,7 +3,7 @@
package isatty
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
// terminal. This is also always false on this environment.
func IsCygwinTerminal(fd uintptr) bool {
return false

View File

@@ -6,14 +6,8 @@ go:
env:
global:
- QINIU_KODO_TEST=1
- QINIU_ACCESS_KEY="QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm"
- QINIU_SECRET_KEY="Bxckh6FA-Fbs9Yt3i3cbKVK22UPBmAOHJcL95pGz"
- QINIU_TEST_BUCKET="gosdk"
- QINIU_TEST_DOMAIN="gosdk.qiniudn.com"
install:
- export QINIU_SRC=$HOME/gopath/src
- mkdir -p $QINIU_SRC/github.com/qiniu
- export TRAVIS_BUILD_DIR=$QINIU_SRC/github.com/qiniu/api.v7
- cd $TRAVIS_BUILD_DIR
- go get github.com/qiniu/x

View File

@@ -1,4 +1,21 @@
#Changelog
# Changelog
# 7.2.3 (2017-09-25)
* 增加Qiniu的鉴权方式
* 删除prefop域名检测功能
* 暴露分片上传的接口以支持复杂的自定义业务逻辑
## 7.2.2 (2017-09-19)
* 为表单上传和分片上传增加代理支持
* 优化表单上传的crc32计算方式减少内存消耗
* 增加网页图片的Base64上传方式
## 7.2.1 (2017-08-20)
* 设置FormUpload默认支持crc32校验
* ResumeUpload从API层面即支持crc32校验
## 7.2.0 (2017-07-28)
* 重构了v7 SDK的所有代码
## 7.1.0 (2016-6-22)

5
vendor/github.com/qiniu/api.v7/Makefile generated vendored Normal file
View File

@@ -0,0 +1,5 @@
test:
go test -v ./auth/...
go test -v ./conf/...
go test -v ./cdn/...
go test -v ./storage/...

View File

@@ -1,7 +1,7 @@
github.com/qiniu/api.v7 (Qiniu Go SDK v7.x)
===============
[![Build Status](https://travis-ci.org/qiniu/api.v7.svg?branch=develop)](https://travis-ci.org/qiniu/api.v7) [![GoDoc](https://godoc.org/github.com/qiniu/api.v7?status.svg)](https://godoc.org/github.com/qiniu/api.v7)
[![Build Status](https://travis-ci.org/qiniu/api.v7.svg?branch=master)](https://travis-ci.org/qiniu/api.v7) [![GoDoc](https://godoc.org/github.com/qiniu/api.v7?status.svg)](https://godoc.org/github.com/qiniu/api.v7)
[![Qiniu Logo](http://open.qiniudn.com/logo.png)](http://qiniu.com/)
@@ -10,16 +10,11 @@ github.com/qiniu/api.v7 (Qiniu Go SDK v7.x)
```
go get -u github.com/qiniu/api.v7
```
如果碰到golang.org/x/net/context 不能下载,请把 http://devtools.qiniu.com/golang.org.x.net.context.tgz 下载到代码目录下并解压到src目录或者直接下载全部 http://devtools.qiniu.com/qiniu_api_v7.tgz。
# 使用文档
# 文档
## KODO Blob Storage (七牛对象存储)
[七牛SDK文档站](https://developer.qiniu.com/kodo/sdk/1238/go) 或者 [项目WIKI](https://github.com/qiniu/api.v7/wiki)
* [github.com/qiniu/api.v7/kodo](http://godoc.org/github.com/qiniu/api.v7/kodo)
* [github.com/qiniu/api.v7/kodocli](http://godoc.org/github.com/qiniu/api.v7/kodocli)
如果您是在业务服务器(服务器端)调用七牛云存储的服务,请使用 [github.com/qiniu/api.v7/kodo](http://godoc.org/github.com/qiniu/api.v7/kodo)。
如果您是在客户端比如Android/iOS 设备、Windows/Mac/Linux 桌面环境)调用七牛云存储的服务,请使用 [github.com/qiniu/api.v7/kodocli](http://godoc.org/github.com/qiniu/api.v7/kodocli)。注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,会影响您的数据安全。
# 示例
[参考代码](https://github.com/qiniu/api.v7/tree/master/examples)

View File

@@ -1,99 +0,0 @@
package api
import (
. "context"
"sync"
"time"
"github.com/qiniu/x/rpc.v7"
)
const DefaultApiHost string = "http://uc.qbox.me"
var (
bucketMu sync.RWMutex
bucketCache = make(map[string]BucketInfo)
)
type Client struct {
*rpc.Client
host string
scheme string
}
func NewClient(host string, scheme string) *Client {
if host == "" {
host = DefaultApiHost
}
client := rpc.DefaultClient
return &Client{&client, host, scheme}
}
type BucketInfo struct {
UpHosts []string `json:"up"`
IoHost string `json:"io"`
Expire int64 `json:"expire"` // expire == 0 means no expire
}
func (p *Client) GetBucketInfo(ak, bucketName string) (ret BucketInfo, err error) {
key := ak + ":" + bucketName + ":" + p.scheme
bucketMu.RLock()
bucketInfo, ok := bucketCache[key]
bucketMu.RUnlock()
if ok && (bucketInfo.Expire == 0 || bucketInfo.Expire > time.Now().Unix()) {
ret = bucketInfo
return
}
hostInfo, err := p.bucketHosts(ak, bucketName)
if err != nil {
return
}
ret.Expire = time.Now().Unix() + hostInfo.Ttl
if p.scheme == "https" {
ret.UpHosts = hostInfo.Https["up"]
if iohosts, ok := hostInfo.Https["io"]; ok && len(iohosts) != 0 {
ret.IoHost = iohosts[0]
}
} else {
ret.UpHosts = hostInfo.Http["up"]
if iohosts, ok := hostInfo.Http["io"]; ok && len(iohosts) != 0 {
ret.IoHost = iohosts[0]
}
}
bucketMu.Lock()
bucketCache[key] = ret
bucketMu.Unlock()
return
}
type HostsInfo struct {
Ttl int64 `json:"ttl"`
Http map[string][]string `json:"http"`
Https map[string][]string `json:"https"`
}
/*
请求包:
GET /v1/query?ak=<ak>&&bucket=<bucket>
返回包:
200 OK {
"ttl": <ttl>, // 有效时间
"http": {
"up": [],
"io": [], // 当bucket为global时我们不需要iohost, io缺省
},
"https": {
"up": [],
"io": [], // 当bucket为global时我们不需要iohost, io缺省
}
}
*/
func (p *Client) bucketHosts(ak, bucket string) (info HostsInfo, err error) {
ctx := Background()
err = p.CallWithForm(ctx, &info, "GET", p.host+"/v1/query", map[string][]string{
"ak": []string{ak},
"bucket": []string{bucket},
})
return
}

2
vendor/github.com/qiniu/api.v7/auth/qbox/doc.go generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// qbox 包提供了该SDK需要的相关鉴权方法
package qbox

View File

@@ -4,61 +4,45 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net/http"
. "github.com/qiniu/api.v7/conf"
"github.com/qiniu/x/bytes.v7/seekable"
)
// ----------------------------------------------------------
// Mac 七牛AK/SK的对象AK/SK可以从 https://portal.qiniu.com/user/key 获取。
type Mac struct {
AccessKey string
SecretKey []byte
}
// NewMac 构建一个新的拥有AK/SK的对象
func NewMac(accessKey, secretKey string) (mac *Mac) {
if accessKey == "" {
accessKey, secretKey = ACCESS_KEY, SECRET_KEY
}
return &Mac{accessKey, []byte(secretKey)}
}
// Sign 对数据进行签名,一般用于私有空间下载用途
func (mac *Mac) Sign(data []byte) (token string) {
h := hmac.New(sha1.New, mac.SecretKey)
h.Write(data)
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
return mac.AccessKey + ":" + sign[:27]
return fmt.Sprintf("%s:%s", mac.AccessKey, sign)
}
// SignWithData 对数据进行签名,一般用于上传凭证的生成用途
func (mac *Mac) SignWithData(b []byte) (token string) {
blen := base64.URLEncoding.EncodedLen(len(b))
key := mac.AccessKey
nkey := len(key)
ret := make([]byte, nkey+30+blen)
base64.URLEncoding.Encode(ret[nkey+30:], b)
encodedData := base64.URLEncoding.EncodeToString(b)
h := hmac.New(sha1.New, mac.SecretKey)
h.Write(ret[nkey+30:])
h.Write([]byte(encodedData))
digest := h.Sum(nil)
copy(ret, key)
ret[nkey] = ':'
base64.URLEncoding.Encode(ret[nkey+1:], digest)
ret[nkey+29] = ':'
return string(ret)
sign := base64.URLEncoding.EncodeToString(digest)
return fmt.Sprintf("%s:%s:%s", mac.AccessKey, sign, encodedData)
}
func (mac *Mac) SignRequest(req *http.Request, incbody bool) (token string, err error) {
// SignRequest 对数据进行签名,一般用于管理凭证的生成
func (mac *Mac) SignRequest(req *http.Request) (token string, err error) {
h := hmac.New(sha1.New, mac.SecretKey)
u := req.URL
@@ -68,7 +52,7 @@ func (mac *Mac) SignRequest(req *http.Request, incbody bool) (token string, err
}
io.WriteString(h, data+"\n")
if incbody {
if incBody(req) {
s2, err2 := seekable.New(req)
if err2 != nil {
return "", err2
@@ -77,18 +61,74 @@ func (mac *Mac) SignRequest(req *http.Request, incbody bool) (token string, err
}
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
token = mac.AccessKey + ":" + sign
token = fmt.Sprintf("%s:%s", mac.AccessKey, sign)
return
}
func (mac *Mac) VerifyCallback(req *http.Request) (bool, error) {
// SignRequestV2 对数据进行签名,一般用于高级管理凭证的生成
func (mac *Mac) SignRequestV2(req *http.Request) (token string, err error) {
h := hmac.New(sha1.New, mac.SecretKey)
u := req.URL
//write method path?query
io.WriteString(h, fmt.Sprintf("%s %s", req.Method, u.Path))
if u.RawQuery != "" {
io.WriteString(h, "?")
io.WriteString(h, u.RawQuery)
}
//write host and posrt
io.WriteString(h, "\nHost: ")
io.WriteString(h, req.Host)
if req.URL.Port() != "" {
io.WriteString(h, ":")
io.WriteString(h, req.URL.Port())
}
//write content type
contentType := req.Header.Get("Content-Type")
if contentType != "" {
io.WriteString(h, "\n")
io.WriteString(h, fmt.Sprintf("Content-Type: %s", contentType))
}
io.WriteString(h, "\n\n")
//write body
if incBodyV2(req) {
s2, err2 := seekable.New(req)
if err2 != nil {
return "", err2
}
h.Write(s2.Bytes())
}
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
token = fmt.Sprintf("%s:%s", mac.AccessKey, sign)
return
}
// 管理凭证生成时是否同时对request body进行签名
func incBody(req *http.Request) bool {
return req.Body != nil &&
req.Header.Get("Content-Type") == "application/x-www-form-urlencoded"
}
func incBodyV2(req *http.Request) bool {
contentType := req.Header.Get("Content-Type")
return req.Body != nil && (contentType == "application/x-www-form-urlencoded" ||
contentType == "application/json")
}
// VerifyCallback 验证上传回调请求是否来自七牛
func (mac *Mac) VerifyCallback(req *http.Request) (bool, error) {
auth := req.Header.Get("Authorization")
if auth == "" {
return false, nil
}
token, err := mac.SignRequest(req, true)
token, err := mac.SignRequest(req)
if err != nil {
return false, err
}
@@ -96,76 +136,17 @@ func (mac *Mac) VerifyCallback(req *http.Request) (bool, error) {
return auth == "QBox "+token, nil
}
// ---------------------------------------------------------------------------------------
// Sign 一般用于下载凭证的签名
func Sign(mac *Mac, data []byte) string {
if mac == nil {
mac = NewMac(ACCESS_KEY, SECRET_KEY)
}
return mac.Sign(data)
}
// SignWithData 一般用于上传凭证的签名
func SignWithData(mac *Mac, data []byte) string {
if mac == nil {
mac = NewMac(ACCESS_KEY, SECRET_KEY)
}
return mac.SignWithData(data)
}
// ---------------------------------------------------------------------------------------
type Transport struct {
mac Mac
Transport http.RoundTripper
// VerifyCallback 验证上传回调请求是否来自七牛
func VerifyCallback(mac *Mac, req *http.Request) (bool, error) {
return mac.VerifyCallback(req)
}
func incBody(req *http.Request) bool {
if req.Body == nil {
return false
}
if ct, ok := req.Header["Content-Type"]; ok {
switch ct[0] {
case "application/x-www-form-urlencoded":
return true
}
}
return false
}
func (t *Transport) NestedObject() interface{} {
return t.Transport
}
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
token, err := t.mac.SignRequest(req, incBody(req))
if err != nil {
return
}
req.Header.Set("Authorization", "QBox "+token)
return t.Transport.RoundTrip(req)
}
func NewTransport(mac *Mac, transport http.RoundTripper) *Transport {
if mac == nil {
mac = NewMac(ACCESS_KEY, SECRET_KEY)
}
if transport == nil {
transport = http.DefaultTransport
}
t := &Transport{mac: *mac, Transport: transport}
return t
}
func NewClient(mac *Mac, transport http.RoundTripper) *http.Client {
t := NewTransport(mac, transport)
return &http.Client{Transport: t}
}
// ---------------------------------------------------------------------------------------

View File

@@ -7,10 +7,9 @@ import (
"time"
)
// CreateTimestampAntileechURL 构建带时间戳防盗链的链接
// encryptKey 七牛防盗链key
func CreateTimestampAntileechURL(urlStr string, encryptKey string, durationInSeconds int64) (antileechURL string, err error) {
// CreateTimestampAntileechURL 用来构建七牛CDN时间戳防盗链的访问链接
func CreateTimestampAntileechURL(urlStr string, encryptKey string,
durationInSeconds int64) (antileechURL string, err error) {
u, err := url.Parse(urlStr)
if err != nil {
return
@@ -27,7 +26,6 @@ func CreateTimestampAntileechURL(urlStr string, encryptKey string, durationInSec
if u.RawQuery == "" {
antileechURL = u.String() + "?" + q.Encode()
} else {
antileechURL = u.String() + "&" + q.Encode()
}

View File

@@ -6,7 +6,7 @@ import (
func TestCreateTimestampAntiLeech(t *testing.T) {
type args struct {
urlStr string
urlStr string
encryptKey string
durationInSeconds int64
}
@@ -18,20 +18,21 @@ func TestCreateTimestampAntiLeech(t *testing.T) {
{
name: "antileech_1",
args: args{
urlStr: "http://www.abc.com/abc.jpg?stat",
encryptKey: "abc",
durationInSeconds: 20,
urlStr: "http://www.example.com/testfile.jpg",
encryptKey: "abc123",
durationInSeconds: 3600,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := CreateTimestampAntileechURL(tt.args.urlStr, tt.args.encryptKey, tt.args.durationInSeconds)
targetUrl, err := CreateTimestampAntileechURL(tt.args.urlStr, tt.args.encryptKey, tt.args.durationInSeconds)
if (err != nil) != tt.wantErr {
t.Errorf("CreateTimestampAntiLeech() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log(targetUrl)
})
}
}

View File

@@ -3,73 +3,72 @@ package cdn
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/qiniu/api.v7/auth/qbox"
. "github.com/qiniu/api.v7/conf"
)
// Fusion CDN服务域名
var (
FUSION_HOST = "http://fusion.qiniuapi.com"
FusionHost = "http://fusion.qiniuapi.com"
)
/* TrafficReqBody
// CdnManager 提供了文件和目录刷新,文件预取,获取域名带宽和流量数据,获取域名日志列表等功能
type CdnManager struct {
mac *qbox.Mac
}
批量查询带宽/流量 请求内容
// NewCdnManager 用来构建一个新的 CdnManager
func NewCdnManager(mac *qbox.Mac) *CdnManager {
return &CdnManager{mac: mac}
}
StartDate string 开始日期例如2016-07-01
EndDate string 结束日期例如2016-07-03
Granularity string 粒度取值5min hour day
Domains string 域名列表,以 ;分割
*/
type TrafficReqBody struct {
// TrafficReq 为批量查询带宽/流量的API请求内容
// StartDate 开始日期,格式例如2016-07-01
// EndDate 结束日期格式例如2016-07-03
// Granularity 取值粒度取值可选值5min/hour/day
// Domains 域名列表,彼此用 ; 连接
type TrafficReq struct {
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Granularity string `json:"granularity"`
Domains string `json:"domains"`
}
// TrafficResp
// 带宽/流量查询响应内容
// TrafficResp 为带宽/流量查询响应内容
type TrafficResp struct {
Code int `json:"code"`
Error string `json:"error"`
Time []string `json:"time,omitempty"`
Data map[string]TrafficRespData `json:"data,omitempty"`
Code int `json:"code"`
Error string `json:"error"`
Time []string `json:"time,omitempty"`
Data map[string]TrafficData `json:"data,omitempty"`
}
// TrafficRespData
// 带宽/流量数据
type TrafficRespData struct {
// TrafficData 为带宽/流量数据
type TrafficData struct {
DomainChina []int `json:"china"`
DomainOversea []int `json:"oversea"`
}
/*
// BandWidth
获取域名访问带宽数据
http://developer.qiniu.com/article/fusion/api/traffic-bandwidth.html
StartDate string 必须 开始日期例如2016-07-01
EndDate string 必须 结束日期例如2016-07-03
Granularity string 必须 粒度取值5min hour day
Domains []string 必须 域名列表
*/
func GetBandWidthData(startDate, endDate, granularity string, domainList []string) (bandwidthData TrafficResp, err error) {
// GetBandwidthData 方法用来获取域名访问带宽数据
// StartDate string 必须 开始日期例如2016-07-01
// EndDate string 必须 结束日期例如2016-07-03
// Granularity string 必须 粒度取值5min hour day
// Domains []string 必须 域名列表
func (m *CdnManager) GetBandwidthData(startDate, endDate, granularity string,
domainList []string) (bandwidthData TrafficResp, err error) {
domains := strings.Join(domainList, ";")
reqBody := TrafficReqBody{
reqBody := TrafficReq{
StartDate: startDate,
EndDate: endDate,
Granularity: granularity,
Domains: domains,
}
resData, reqErr := postRequest("v2/tune/bandwidth", reqBody)
resData, reqErr := postRequest(m.mac, "/v2/tune/bandwidth", reqBody)
if reqErr != nil {
err = reqErr
return
@@ -82,27 +81,22 @@ func GetBandWidthData(startDate, endDate, granularity string, domainList []strin
return
}
/* Flux
获取域名访问流量数据
http://developer.qiniu.com/article/fusion/api/traffic-bandwidth.html
StartDate string 必须 开始日期例如2016-07-01
EndDate string 必须 结束日期例如2016-07-03
Granularity string 必须 粒度取值5min hour day
Domains []string 必须 域名列表
*/
func GetFluxData(startDate, endDate, granularity string, domainList []string) (fluxData TrafficResp, err error) {
// GetFluxData 方法用来获取域名访问流量数据
// StartDate string 必须 开始日期例如2016-07-01
// EndDate string 必须 结束日期例如2016-07-03
// Granularity string 必须 粒度取值5min hour day
// Domains []string 必须 域名列表
func (m *CdnManager) GetFluxData(startDate, endDate, granularity string,
domainList []string) (fluxData TrafficResp, err error) {
domains := strings.Join(domainList, ";")
reqBody := TrafficReqBody{
reqBody := TrafficReq{
StartDate: startDate,
EndDate: endDate,
Granularity: granularity,
Domains: domains,
}
resData, reqErr := postRequest("v2/tune/flux", reqBody)
resData, reqErr := postRequest(m.mac, "/v2/tune/flux", reqBody)
if reqErr != nil {
err = reqErr
return
@@ -117,43 +111,46 @@ func GetFluxData(startDate, endDate, granularity string, domainList []string) (f
return
}
// RefreshReq
// 缓存刷新请求内容
// RefreshReq 为缓存刷新请求内容
type RefreshReq struct {
Urls []string `json:"urls"`
Dirs []string `json:"dirs"`
}
// RefreshResp
// 缓存刷新响应内容
// RefreshResp 缓存刷新响应内容
type RefreshResp struct {
Code int `json:"code"`
Error string `json:"error"`
RequestID string `json:"requestId,omitempty"`
InvalidUrls []string `json:"invalidUrls,omitempty"`
InvalidDirs []string `json:"invalidDirs,omitempty"`
UrlQuotaDay int `json:"urlQuotaDay,omitempty"`
UrlSurplusDay int `json:"urlSurplusDay,omitempty"`
URLQuotaDay int `json:"urlQuotaDay,omitempty"`
URLSurplusDay int `json:"urlSurplusDay,omitempty"`
DirQuotaDay int `json:"dirQuotaDay,omitempty"`
DirSurplusDay int `json:"dirSurplusDay,omitempty"`
}
/* RefreshUrlsAndDirs
刷新链接列表每次最多不可以超过100条链接
http://developer.qiniu.com/article/fusion/api/refresh.html
urls 要刷新的单个url列表总数不超过100条单个url即一个具体的url例如http://bar.foo.com/index.html
dirs 要刷新的目录url列表总数不超过10条目录dir即表示一个目录级的url例如http://bar.foo.com/dir/也支持在尾部使用通配符例如http://bar.foo.com/dir/*
*/
func RefreshUrlsAndDirs(urls, dirs []string) (result RefreshResp, err error) {
// RefreshUrlsAndDirs 方法用来刷新文件或目录
// urls 要刷新的单个url列表单次方法调用总数不超过100条单个url即一个具体的url
// 例如http://bar.foo.com/index.html
// dirs 要刷新的目录url列表单次方法调用总数不超过10条目录dir即表示一个目录级的url
// 例如http://bar.foo.com/dir/
func (m *CdnManager) RefreshUrlsAndDirs(urls, dirs []string) (result RefreshResp, err error) {
if len(urls) > 100 {
err = errors.New("urls count exceeds the limit of 100")
return
}
if len(dirs) > 10 {
err = errors.New("dirs count exceeds the limit of 10")
return
}
reqBody := RefreshReq{
Urls: urls,
Dirs: dirs,
}
resData, reqErr := postRequest("v2/tune/refresh", reqBody)
resData, reqErr := postRequest(m.mac, "/v2/tune/refresh", reqBody)
if reqErr != nil {
err = reqErr
return
@@ -167,26 +164,22 @@ func RefreshUrlsAndDirs(urls, dirs []string) (result RefreshResp, err error) {
return
}
// RefreshUrls
// 刷新文件
func RefreshUrls(urls []string) (result RefreshResp, err error) {
return RefreshUrlsAndDirs(urls, nil)
// RefreshUrls 刷新文件
func (m *CdnManager) RefreshUrls(urls []string) (result RefreshResp, err error) {
return m.RefreshUrlsAndDirs(urls, nil)
}
// RefreshDirs
// 刷新目录
func RefreshDirs(dirs []string) (result RefreshResp, err error) {
return RefreshUrlsAndDirs(nil, dirs)
// RefreshDirs 刷新目录
func (m *CdnManager) RefreshDirs(dirs []string) (result RefreshResp, err error) {
return m.RefreshUrlsAndDirs(nil, dirs)
}
// PrefetchReq
// 文件预取请求内容
// PrefetchReq 文件预取请求内容
type PrefetchReq struct {
Urls []string `json:"urls"`
}
// PrefetchResp
// 文件预取响应内容
// PrefetchResp 文件预取响应内容
type PrefetchResp struct {
Code int `json:"code"`
Error string `json:"error"`
@@ -196,16 +189,18 @@ type PrefetchResp struct {
SurplusDay int `json:"surplusDay,omitempty"`
}
// PrefetchUrls
// 预取文件链接每次最多不可以超过100条
// http://developer.qiniu.com/article/fusion/api/prefetch.html
func PrefetchUrls(urls []string) (result PrefetchResp, err error) {
// PrefetchUrls 预取文件链接每次最多不可以超过100条
func (m *CdnManager) PrefetchUrls(urls []string) (result PrefetchResp, err error) {
if len(urls) > 100 {
err = errors.New("urls count exceeds the limit of 100")
return
}
reqBody := PrefetchReq{
Urls: urls,
}
resData, reqErr := postRequest("v2/tune/prefetch", reqBody)
resData, reqErr := postRequest(m.mac, "/v2/tune/prefetch", reqBody)
if reqErr != nil {
err = reqErr
return
@@ -220,12 +215,59 @@ func PrefetchUrls(urls []string) (result PrefetchResp, err error) {
return
}
// RequestWithBody
// 带body对api发出请求并且返回response body
func postRequest(path string, body interface{}) (resData []byte, err error) {
// ListLogRequest 日志下载请求内容
type ListLogRequest struct {
Day string `json:"day"`
Domains string `json:"domains"`
}
urlStr := fmt.Sprintf("%s/%s", FUSION_HOST, path)
// ListLogResult 日志下载相应内容
type ListLogResult struct {
Code int `json:"code"`
Error string `json:"error"`
Data map[string][]LogDomainInfo `json:"data"`
}
// LogDomainInfo 日志下载信息
type LogDomainInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModifiedTime int64 `json:"mtime"`
URL string `json:"url"`
}
// GetCdnLogList 获取CDN域名访问日志的下载链接
func (m *CdnManager) GetCdnLogList(day string, domains []string) (
listLogResult ListLogResult, err error) {
//new log query request
logReq := ListLogRequest{
Day: day,
Domains: strings.Join(domains, ";"),
}
resData, reqErr := postRequest(m.mac, "/v2/tune/log/list", logReq)
if reqErr != nil {
err = fmt.Errorf("get response error, %s", reqErr)
return
}
if decodeErr := json.Unmarshal(resData, &listLogResult); decodeErr != nil {
err = fmt.Errorf("get response error, %s", decodeErr)
return
}
if listLogResult.Error != "" {
err = fmt.Errorf("get log list error, %d %s", listLogResult.Code, listLogResult.Error)
return
}
return
}
// RequestWithBody 带body对api发出请求并且返回response body
func postRequest(mac *qbox.Mac, path string, body interface{}) (resData []byte,
err error) {
urlStr := fmt.Sprintf("%s%s", FusionHost, path)
reqData, _ := json.Marshal(body)
req, reqErr := http.NewRequest("POST", urlStr, bytes.NewReader(reqData))
if reqErr != nil {
@@ -233,8 +275,7 @@ func postRequest(path string, body interface{}) (resData []byte, err error) {
return
}
mac := qbox.NewMac(ACCESS_KEY, SECRET_KEY)
accessToken, signErr := mac.SignRequest(req, false)
accessToken, signErr := mac.SignRequest(req)
if signErr != nil {
err = signErr
return
@@ -243,9 +284,9 @@ func postRequest(path string, body interface{}) (resData []byte, err error) {
req.Header.Add("Authorization", "QBox "+accessToken)
req.Header.Add("Content-Type", "application/json")
resp, httpErr := http.DefaultClient.Do(req)
if httpErr != nil {
err = httpErr
resp, respErr := http.DefaultClient.Do(req)
if respErr != nil {
err = respErr
return
}
defer resp.Body.Close()

View File

@@ -1,75 +1,86 @@
package cdn
import (
"math/rand"
"os"
"reflect"
"strconv"
"testing"
"time"
"github.com/qiniu/api.v7/kodo"
"github.com/qiniu/api.v7/auth/qbox"
)
//global variables
var (
ak = os.Getenv("QINIU_ACCESS_KEY")
sk = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
testBucketName = os.Getenv("QINIU_TEST_BUCKET")
ak = os.Getenv("QINIU_ACCESS_KEY")
sk = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
testDate = time.Now().AddDate(0, 0, -3).Format("2006-01-02")
bucket = newBucket()
client *kodo.Client
testKey = "fusionTest"
testURL string
layout = "2006-01-02"
now = time.Now()
startDate = now.AddDate(0, 0, -2).Format(layout)
endDate = now.AddDate(0, 0, -1).Format(layout)
logDate = now.AddDate(0, 0, -1).Format(layout)
testUrls = []string{
"http://gosdk.qiniudn.com/qiniu1.png",
"http://gosdk.qiniudn.com/qiniu2.png",
}
testDirs = []string{
"http://gosdk.qiniudn.com/dir1/",
"http://gosdk.qiniudn.com/dir2/",
}
)
func init() {
kodo.SetMac(ak, sk)
rand.Seed(time.Now().UnixNano())
testKey += strconv.Itoa(rand.Int())
bucket.PutFile(nil, nil, testKey, "doc.go", nil)
testURL = domain + "/" + testKey
var mac *qbox.Mac
var cdnManager *CdnManager
func init() {
if ak == "" || sk == "" {
panic("please run ./test-env.sh first")
}
mac = qbox.NewMac(ak, sk)
cdnManager = NewCdnManager(mac)
}
func TestGetBandWidthData(t *testing.T) {
//TestGetBandwidthData
func TestGetBandwidthData(t *testing.T) {
type args struct {
startDate string
endDate string
granularity string
domainList []string
}
tests := []struct {
name string
args args
wantTraffic TrafficResp
wantErr bool
testCases := []struct {
name string
args args
wantCode int
}{
{
name: "BandWidthTest_1",
name: "CdnManager_TestGetBandwidthData",
args: args{
testDate,
testDate,
startDate,
endDate,
"5min",
[]string{domain},
},
wantCode: 200,
},
}
kodo.SetMac(ak, sk)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := GetBandWidthData(tt.args.startDate, tt.args.endDate, tt.args.granularity, tt.args.domainList)
if (err != nil) != tt.wantErr {
t.Errorf("GetBandWidthData() error = %v, wantErr %v", err, tt.wantErr)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ret, err := cdnManager.GetBandwidthData(tc.args.startDate, tc.args.endDate,
tc.args.granularity, tc.args.domainList)
if err != nil || ret.Code != tc.wantCode {
t.Errorf("GetBandwidth() error = %v, %v", err, ret.Error)
return
}
})
}
}
//TestGetFluxData
func TestGetFluxData(t *testing.T) {
type args struct {
startDate string
@@ -77,167 +88,160 @@ func TestGetFluxData(t *testing.T) {
granularity string
domainList []string
}
tests := []struct {
name string
args args
wantTraffic TrafficResp
wantErr bool
testCases := []struct {
name string
args args
wantCode int
}{
{
name: "BandWidthTest_1",
name: "CdnManager_TestGetFluxData",
args: args{
testDate,
testDate,
startDate,
endDate,
"5min",
[]string{domain},
},
wantCode: 200,
},
}
kodo.SetMac(ak, sk)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := GetFluxData(tt.args.startDate, tt.args.endDate, tt.args.granularity, tt.args.domainList)
if (err != nil) != tt.wantErr {
t.Errorf("GetFluxData() error = %v, wantErr %v", err, tt.wantErr)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ret, err := cdnManager.GetFluxData(tc.args.startDate, tc.args.endDate,
tc.args.granularity, tc.args.domainList)
if err != nil || ret.Code != tc.wantCode {
t.Errorf("GetFlux() error = %v, %v", err, ret.Error)
return
}
})
}
}
func TestRefreshUrlsAndDirs(t *testing.T) {
kodo.SetMac(ak, sk)
type args struct {
urls []string
dirs []string
}
tests := []struct {
name string
args args
wantResult RefreshResp
wantErr bool
}{
{
name: "refresh_test_1",
args: args{
urls: []string{testURL},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RefreshUrlsAndDirs(tt.args.urls, tt.args.dirs)
if (err != nil) != tt.wantErr {
t.Errorf("RefreshUrlsAndDirs() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
//TestRefreshUrls
func TestRefreshUrls(t *testing.T) {
type args struct {
urls []string
}
tests := []struct {
name string
args args
wantResult RefreshResp
wantErr bool
testCases := []struct {
name string
args args
wantCode int
}{
{
name: "refresh_test_1",
name: "CdnManager_TestRefresUrls",
args: args{
urls: []string{testURL},
urls: testUrls,
},
wantErr: false,
wantCode: 200,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RefreshUrls(tt.args.urls)
if (err != nil) != tt.wantErr {
t.Errorf("RefreshUrls() error = %v, wantErr %v", err, tt.wantErr)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ret, err := cdnManager.RefreshUrls(tc.args.urls)
if err != nil || ret.Code != tc.wantCode {
t.Errorf("RefreshUrls() error = %v, %v", err, ret.Error)
return
}
})
}
}
//TestRefreshDirs
func TestRefreshDirs(t *testing.T) {
type args struct {
dirs []string
}
tests := []struct {
name string
args args
wantResult RefreshResp
wantErr bool
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotResult, err := RefreshDirs(tt.args.dirs)
if (err != nil) != tt.wantErr {
t.Errorf("RefreshDirs() error = %v, wantErr %v", err, tt.wantErr)
testCases := []struct {
name string
args args
wantCode int
}{
{
name: "CdnManager_TestRefreshDirs",
args: args{
dirs: testDirs,
},
wantCode: 200,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ret, err := cdnManager.RefreshDirs(tc.args.dirs)
if err != nil || ret.Code != tc.wantCode {
if ret.Error == "refresh dir limit error" {
t.Logf("RefreshDirs() error=%v", ret.Error)
} else {
t.Errorf("RefreshDirs() error = %v, %v", err, ret.Error)
}
return
}
if !reflect.DeepEqual(gotResult, tt.wantResult) {
t.Errorf("RefreshDirs() = %v, want %v", gotResult, tt.wantResult)
}
})
}
}
//TestPrefetchUrls
func TestPrefetchUrls(t *testing.T) {
type args struct {
urls []string
}
tests := []struct {
name string
args args
wantResult PrefetchResp
wantErr bool
testCases := []struct {
name string
args args
wantCode int
}{
{
name: "refresh_test_1",
name: "CdnManager_PrefetchUrls",
args: args{
urls: []string{testURL},
urls: testUrls,
},
wantErr: false,
wantCode: 200,
},
}
kodo.SetMac(ak, sk)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := PrefetchUrls(tt.args.urls)
if (err != nil) != tt.wantErr {
t.Errorf("PrefetchUrls() error = %v, wantErr %v", err, tt.wantErr)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ret, err := cdnManager.PrefetchUrls(tc.args.urls)
if err != nil || ret.Code != tc.wantCode {
t.Errorf("PrefetchUrls() error = %v, %v", err, ret.Error)
return
}
})
}
}
func newBucket() (bucket kodo.Bucket) {
ak := os.Getenv("QINIU_ACCESS_KEY")
sk := os.Getenv("QINIU_SECRET_KEY")
if ak == "" || sk == "" {
panic("require ACCESS_KEY & SECRET_KEY")
//TestGetCdnLogList
func TestGetCdnLogList(t *testing.T) {
type args struct {
date string
domains []string
}
kodo.SetMac(ak, sk)
testBucketName = os.Getenv("QINIU_TEST_BUCKET")
domain = os.Getenv("QINIU_TEST_DOMAIN")
if testBucketName == "" || domain == "" {
panic("require test env")
testCases := []struct {
name string
args args
}{
{
name: "CdnManager_TestGetCdnLogList",
args: args{
date: logDate,
domains: []string{domain},
},
},
}
client = kodo.NewWithoutZone(nil)
return client.Bucket(testBucketName)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := cdnManager.GetCdnLogList(tc.args.date, tc.args.domains)
if err != nil {
t.Errorf("GetCdnLogList() error = %v", err)
return
}
})
}
}

View File

@@ -1,25 +1,3 @@
/*
包 github.com/qiniu/api.v7/cdn 提供了七牛CDN的API功能
首先,我们要配置下 AccessKey/SecretKey,
import "github.com/qiniu/api.v7/kodo"
kodo.SetMac("ak", "sk")
设置了AccessKey/SecretKey 就可以使用cdn的各类功能
比如我们要生成一个带时间戳防盗链的链接:
q :=url.Values{}// url.Values 请求参数
link, err := cdn.CreateTimestampAntileechURL(""http://www.qiniu.com/abc/bcc/aa-s.mp4?x=2&y=3", "encryptedkey", 20)
if err != nil {
fmt.Println(err)
}
fmt.Println(link)
又或者我们要列出CDN日志及其下载地址
resp, err := cdn.GetCdnLogList("2016-12-26", "x-mas.com")
if err != nil {
fmt.Println(err)
}
fmt.Println(resp)
*/
// cdn 包提供了 Fusion CDN的常见功能。相关功能的文档参考https://developer.qiniu.com/fusion。
// 目前提供了文件和目录刷新,文件预取,获取域名带宽和流量数据,获取域名日志列表等功能。
package cdn

View File

@@ -1,92 +0,0 @@
package cdn
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/qiniu/api.v7/auth/qbox"
)
const (
LIST_LOG_API = "http://fusion.qiniuapi.com/v2/tune/log/list"
)
// ListLogRequest 日志下载请求内容
type ListLogRequest struct {
Day string `json:"day"`
Domains string `json:"domains"`
}
// ListLogResult 日志下载相应内容
type ListLogResult struct {
Code int `json:"code"`
Error string `json:"error"`
Data map[string][]LogDomainInfo `json:"data"`
}
// LogDomainInfo 日志下载信息
type LogDomainInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModifiedTime int64 `json:"mtime"`
URL string `json:"url"`
}
// GetCdnLogList 获取CDN域名访问日志的下载链接
// http://developer.qiniu.com/article/fusion/api/log.html
func GetCdnLogList(date, domains string) (domainLogs []LogDomainInfo, err error) {
//new log query request
logReq := ListLogRequest{
Day: date,
Domains: domains,
}
logReqBytes, _ := json.Marshal(&logReq)
req, reqErr := http.NewRequest("POST", LIST_LOG_API, bytes.NewReader(logReqBytes))
if reqErr != nil {
err = fmt.Errorf("New request error, %s", reqErr)
return
}
mac := qbox.NewMac("", "")
token, signErr := mac.SignRequest(req, false)
if signErr != nil {
err = signErr
return
}
req.Header.Add("Authorization", "QBox "+token)
req.Header.Add("Content-Type", "application/json")
resp, respErr := http.DefaultClient.Do(req)
if respErr != nil {
err = fmt.Errorf("Get response error, %s", respErr)
return
}
defer resp.Body.Close()
listLogResult := ListLogResult{}
decoder := json.NewDecoder(resp.Body)
if decodeErr := decoder.Decode(&listLogResult); decodeErr != nil {
err = fmt.Errorf("Parse response error, %s", decodeErr)
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("Get log list error, %d %s", listLogResult.Code, listLogResult.Error)
return
}
domainItems := strings.Split(domains, ";")
for _, domain := range domainItems {
for _, v := range listLogResult.Data[domain] {
domainLogs = append(domainLogs, v)
}
}
return
}

View File

@@ -1,48 +0,0 @@
package cdn
import (
"fmt"
"reflect"
"testing"
"github.com/qiniu/api.v7/kodo"
)
func init() {
kodo.SetMac(ak, sk)
}
func TestGetCdnLogList(t *testing.T) {
type args struct {
date string
domains string
}
tests := []struct {
name string
args args
wantDomainLogs []LogDomainInfo
wantErr bool
}{
{
name: "getCdnLogListTest",
args: args{
date: testDate,
domains: domain,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDomainLogs, err := GetCdnLogList(tt.args.date, tt.args.domains)
if (err != nil) != tt.wantErr {
t.Errorf("GetCdnLogList() error = %v, wantErr %v", err, tt.wantErr)
return
}
fmt.Println(domain, gotDomainLogs)
if !reflect.DeepEqual(gotDomainLogs, tt.wantDomainLogs) {
t.Errorf("GetCdnLogList() = %v, want %v", gotDomainLogs, tt.wantDomainLogs)
}
})
}
}

View File

@@ -9,19 +9,13 @@ import (
"github.com/qiniu/x/rpc.v7"
)
var version = "7.1.0"
var ACCESS_KEY string
var SECRET_KEY string
// ----------------------------------------------------------
var version = "7.2.3"
const (
ctypeAppName = ctype.ALPHA | ctype.DIGIT | ctype.UNDERLINE | ctype.SPACE_BAR | ctype.SUB | ctype.DOT
)
// userApp should be [A-Za-z0-9_\ \-\.]*
//
func SetAppName(userApp string) error {
if userApp != "" && !ctype.IsType(ctypeAppName, userApp) {
return syscall.EINVAL
@@ -34,5 +28,3 @@ func SetAppName(userApp string) error {
func init() {
SetAppName("")
}
// ----------------------------------------------------------

2
vendor/github.com/qiniu/api.v7/conf/doc.go generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// conf 包提供了设置APP名称的方法。该APP名称会被放入API请求的UserAgent中方便后续查询日志分析问题。
package conf

View File

@@ -1,16 +1,13 @@
/*
包 github.com/qiniu/api.v7 是七牛 Go 语言 SDK v7.x 版本
七牛对象存储,我们取了一个好听的名字,叫 KODO Blob Storage。要使用它你主要和以下两个包打交道
包 github.com/qiniu/api.v7 是七牛 Go 语言 SDK v7.x 版本。
import "github.com/qiniu/api.v7/kodo"
import "github.com/qiniu/api.v7/kodocli"
主要提供了存储的数据上传下载管理以及CDN相关的功能。要求Go语言版本>=1.7.0。
如果您是在业务服务器(服务器端)调用七牛云存储的服务,请使用 github.com/qiniu/api.v7/kodo。
Go SDK 中主要包含几个包:
auth 包提供鉴权相关方法conf 包提供配置相关方法cdn包提供CDN相关的功能storage包提供存储相关的功能。
如果您是在客户端比如Android/iOS 设备、Windows/Mac/Linux 桌面环境)调用七牛云存储的服务,请使用 github.com/qiniu/api.v7/kodocli。
注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,
会影响您的数据安全。
*/
package api
@@ -18,6 +15,5 @@ import (
_ "github.com/qiniu/api.v7/auth/qbox"
_ "github.com/qiniu/api.v7/cdn"
_ "github.com/qiniu/api.v7/conf"
_ "github.com/qiniu/api.v7/kodo"
_ "github.com/qiniu/api.v7/kodocli"
_ "github.com/qiniu/api.v7/storage"
)

View File

@@ -0,0 +1,35 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
cfg := storage.Config{}
mac := qbox.NewMac(accessKey, secretKey)
bucketManger := storage.NewBucketManager(mac, &cfg)
siteURL := "http://devtools.qiniu.com"
// 设置镜像存储
err := bucketManger.SetImage(siteURL, bucket)
if err != nil {
fmt.Println(err)
}
// 取消设置镜像存储
err = bucketManger.UnsetImage(bucket)
if err != nil {
fmt.Println(err)
}
}

View File

@@ -0,0 +1,20 @@
package main
import (
"fmt"
"time"
"github.com/qiniu/api.v7/cdn"
)
func main() {
urlStr := "http://image.example.com/qiniu_do_not_delete.gif"
cryptKey := "your crypt key"
deadline := time.Now().Add(time.Second * 3600).Unix()
accessUrl, err := cdn.CreateTimestampAntileechURL(urlStr, cryptKey, deadline)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(accessUrl)
}

View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/cdn"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cdnManager := cdn.NewCdnManager(mac)
startDate := "2017-07-20"
endDate := "2017-07-30"
g := "day"
data, err := cdnManager.GetBandwidthData(startDate, endDate, g, []string{domain})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%v\n", data)
}

View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/cdn"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cdnManager := cdn.NewCdnManager(mac)
startDate := "2017-07-20"
endDate := "2017-07-30"
g := "day"
data, err := cdnManager.GetFluxData(startDate, endDate, g, []string{domain})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%v\n", data)
}

View File

@@ -0,0 +1,37 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/cdn"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cdnManager := cdn.NewCdnManager(mac)
domains := []string{
domain,
}
day := "2017-07-30"
ret, err := cdnManager.GetCdnLogList(day, domains)
if err != nil {
fmt.Println(err)
return
}
domainLogs := ret.Data
for domain, logs := range domainLogs {
fmt.Println(domain)
for _, item := range logs {
fmt.Println(item.Name, item.URL, item.Size, item.ModifiedTime)
}
}
}

View File

@@ -0,0 +1,33 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/cdn"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cdnManager := cdn.NewCdnManager(mac)
// 预取链接单次请求链接不可以超过100个如果超过请分批发送请求
urlsToPrefetch := []string{
"http://if-pbl.qiniudn.com/qiniu.png",
"http://if-pbl.qiniudn.com/github.png",
}
ret, err := cdnManager.PrefetchUrls(urlsToPrefetch)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Code)
fmt.Println(ret.RequestID)
}

View File

@@ -0,0 +1,48 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/cdn"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cdnManager := cdn.NewCdnManager(mac)
//刷新链接单次请求链接不可以超过100个如果超过请分批发送请求
urlsToRefresh := []string{
"http://if-pbl.qiniudn.com/qiniu.png",
"http://if-pbl.qiniudn.com/github.png",
}
ret, err := cdnManager.RefreshUrls(urlsToRefresh)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Code)
fmt.Println(ret.RequestID)
// 刷新目录,刷新目录需要联系七牛技术支持开通权限
// 单次请求链接不可以超过10个如果超过请分批发送请求
dirsToRefresh := []string{
"http://if-pbl.qiniudn.com/images/",
"http://if-pbl.qiniudn.com/static/",
}
ret, err = cdnManager.RefreshDirs(dirsToRefresh)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Code)
fmt.Println(ret.RequestID)
fmt.Println(ret.Error)
}

View File

@@ -0,0 +1,91 @@
package main
import (
"encoding/base64"
"fmt"
"os"
"strings"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
// 简单上传凭证
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
fmt.Println(upToken)
// 设置上传凭证有效期
putPolicy = storage.PutPolicy{
Scope: bucket,
}
putPolicy.Expires = 7200 //示例2小时有效期
upToken = putPolicy.UploadToken(mac)
fmt.Println(upToken)
// 覆盖上传凭证
// 需要覆盖的文件名
keyToOverwrite := "qiniu.mp4"
putPolicy = storage.PutPolicy{
Scope: fmt.Sprintf("%s:%s", bucket, keyToOverwrite),
}
upToken = putPolicy.UploadToken(mac)
fmt.Println(upToken)
// 自定义上传回复凭证
putPolicy = storage.PutPolicy{
Scope: bucket,
ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
}
upToken = putPolicy.UploadToken(mac)
fmt.Println(upToken)
// 带回调业务服务器的凭证(JSON方式)
putPolicy = storage.PutPolicy{
Scope: bucket,
CallbackURL: "http://api.example.com/qiniu/upload/callback",
CallbackBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
CallbackBodyType: "application/json",
}
upToken = putPolicy.UploadToken(mac)
fmt.Println(upToken)
// 带回调业务服务器的凭证URL方式
putPolicy = storage.PutPolicy{
Scope: bucket,
CallbackURL: "http://api.example.com/qiniu/upload/callback",
CallbackBody: "key=$(key)&hash=$(etag)&bucket=$(bucket)&fsize=$(fsize)&name=$(x:name)",
}
upToken = putPolicy.UploadToken(mac)
fmt.Println(upToken)
// 带数据处理的凭证
saveMp4Entry := base64.URLEncoding.EncodeToString([]byte(bucket + ":avthumb_test_target.mp4"))
saveJpgEntry := base64.URLEncoding.EncodeToString([]byte(bucket + ":vframe_test_target.jpg"))
//数据处理指令,支持多个指令
avthumbMp4Fop := "avthumb/mp4|saveas/" + saveMp4Entry
vframeJpgFop := "vframe/jpg/offset/1|saveas/" + saveJpgEntry
//连接多个操作指令
persistentOps := strings.Join([]string{avthumbMp4Fop, vframeJpgFop}, ";")
pipeline := "test"
putPolicy = storage.PutPolicy{
Scope: bucket,
PersistentOps: persistentOps,
PersistentPipeline: pipeline,
PersistentNotifyURL: "http://api.example.com/qiniu/pfop/notify",
}
upToken = putPolicy.UploadToken(mac)
fmt.Println(upToken)
}

View File

@@ -0,0 +1,76 @@
package main
import (
"context"
"fmt"
"net"
"os"
"net/http"
"net/url"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
localFile := "/Users/jemy/Documents/github.png"
key := "github-x.png"
putPolicy := storage.PutPolicy{
Scope: bucket + ":" + key,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
//设置代理
proxyURL := "http://localhost:8888"
proxyURI, _ := url.Parse(proxyURL)
//绑定网卡
nicIP := "100.100.33.138"
dialer := &net.Dialer{
LocalAddr: &net.TCPAddr{
IP: net.ParseIP(nicIP),
},
}
//构建代理client对象
client := http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURI),
Dial: dialer.Dial,
},
}
// 构建表单上传的对象
formUploader := storage.NewFormUploaderEx(&cfg, &rpc.Client{Client: &client})
ret := storage.PutRet{}
// 可选配置
putExtra := storage.PutExtra{
Params: map[string]string{
"x:name": "github logo",
},
}
//putExtra.NoCrc32Check = true
err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Key, ret.Hash)
}

32
vendor/github.com/qiniu/api.v7/examples/prefop.go generated vendored Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
operationManager := storage.NewOperationManager(mac, &cfg)
persistentId := "z0.597f28b445a2650c994bb208"
ret, err := operationManager.Prefop(persistentId)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.String())
}

View File

@@ -0,0 +1,127 @@
package main
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"context"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func md5Hex(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
type ProgressRecord struct {
Progresses []storage.BlkputRet `json:"progresses"`
}
func main() {
localFile := "your local file path"
key := "your file save key"
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
// 必须仔细选择一个能标志上传唯一性的 recordKey 用来记录上传进度
// 我们这里采用 md5(bucket+key+local_path+local_file_last_modified)+".progress" 作为记录上传进度的文件名
fileInfo, statErr := os.Stat(localFile)
if statErr != nil {
fmt.Println(statErr)
return
}
fileSize := fileInfo.Size()
fileLmd := fileInfo.ModTime().UnixNano()
recordKey := md5Hex(fmt.Sprintf("%s:%s:%s:%s", bucket, key, localFile, fileLmd)) + ".progress"
// 指定的进度文件保存目录,实际情况下,请确保该目录存在,而且只用于记录进度文件
recordDir := "/Users/jemy/Temp/progress"
mErr := os.MkdirAll(recordDir, 0755)
if mErr != nil {
fmt.Println("mkdir for record dir error,", mErr)
return
}
recordPath := filepath.Join(recordDir, recordKey)
progressRecord := ProgressRecord{}
// 尝试从旧的进度文件中读取进度
recordFp, openErr := os.Open(recordPath)
if openErr == nil {
progressBytes, readErr := ioutil.ReadAll(recordFp)
if readErr == nil {
mErr := json.Unmarshal(progressBytes, &progressRecord)
if mErr == nil {
// 检查context 是否过期避免701错误
for _, item := range progressRecord.Progresses {
if storage.IsContextExpired(item) {
fmt.Println(item.ExpiredAt)
progressRecord.Progresses = make([]storage.BlkputRet, storage.BlockCount(fileSize))
break
}
}
}
}
recordFp.Close()
}
if len(progressRecord.Progresses) == 0 {
progressRecord.Progresses = make([]storage.BlkputRet, storage.BlockCount(fileSize))
}
resumeUploader := storage.NewResumeUploader(&cfg)
ret := storage.PutRet{}
progressLock := sync.RWMutex{}
putExtra := storage.RputExtra{
Progresses: progressRecord.Progresses,
Notify: func(blkIdx int, blkSize int, ret *storage.BlkputRet) {
progressLock.Lock()
defer progressLock.Unlock()
//将进度序列化,然后写入文件
progressRecord.Progresses[blkIdx] = *ret
progressBytes, _ := json.Marshal(progressRecord)
fmt.Println("write progress file", blkIdx, recordPath)
wErr := ioutil.WriteFile(recordPath, progressBytes, 0644)
if wErr != nil {
fmt.Println("write progress file error,", wErr)
}
},
}
err := resumeUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
if err != nil {
fmt.Println(err)
return
}
//上传成功之后,一定记得删除这个进度文件
os.Remove(recordPath)
fmt.Println(ret.Key, ret.Hash)
}

View File

@@ -0,0 +1,72 @@
package main
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
localFile := "/Users/jemy/Documents/github.png"
key := "qiniu-x.png"
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
//设置代理
proxyURL := "http://localhost:8888"
proxyURI, _ := url.Parse(proxyURL)
//绑定网卡
nicIP := "100.100.33.138"
dialer := &net.Dialer{
LocalAddr: &net.TCPAddr{
IP: net.ParseIP(nicIP),
},
}
//构建代理client对象
client := http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURI),
Dial: dialer.Dial,
},
}
resumeUploader := storage.NewResumeUploaderEx(&cfg, &rpc.Client{Client: &client})
upToken := putPolicy.UploadToken(mac)
ret := storage.PutRet{}
err := resumeUploader.PutFile(context.Background(), &ret, upToken, key, localFile, nil)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Key, ret.Hash)
}

View File

@@ -0,0 +1,64 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
bucketManager := storage.NewBucketManager(mac, &cfg)
chgmKeys := map[string]string{
"github1.png": "image/x-png",
"github2.png": "image/x-png",
"github3.png": "image/x-png",
"github4.png": "image/x-png",
"github5.png": "image/x-png",
}
chgmOps := make([]string, 0, len(chgmKeys))
for key, newMime := range chgmKeys {
chgmOps = append(chgmOps, storage.URIChangeMime(bucket, key, newMime))
}
rets, err := bucketManager.Batch(chgmOps)
if err != nil {
// 遇到错误
if _, ok := err.(*rpc.ErrorInfo); ok {
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
} else {
fmt.Printf("batch error, %s", err)
}
} else {
// 完全成功
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
}
}

View File

@@ -0,0 +1,67 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
bucketManager := storage.NewBucketManager(mac, &cfg)
//每个batch的操作数量不可以超过1000个如果总数量超过1000需要分批发送
chtypeKeys := map[string]int{
"github1.png": 1,
"github2.png": 1,
"github3.png": 1,
"github4.png": 1,
"github5.png": 1,
}
chtypeOps := make([]string, 0, len(chtypeKeys))
for key, fileType := range chtypeKeys {
chtypeOps = append(chtypeOps, storage.URIChangeType(bucket, key, fileType))
}
rets, err := bucketManager.Batch(chtypeOps)
if err != nil {
// 遇到错误
if _, ok := err.(*rpc.ErrorInfo); ok {
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
} else {
fmt.Printf("batch error, %s", err)
}
} else {
// 完全成功
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
}
}

View File

@@ -0,0 +1,67 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
bucketManager := storage.NewBucketManager(mac, &cfg)
//每个batch的操作数量不可以超过1000个如果总数量超过1000需要分批发送
srcBucket := bucket
destBucket := bucket
force := true
copyKeys := map[string]string{
"github1.png": "github1-copy.png",
"github2.png": "github2-copy.png",
"github3.png": "github3-copy.png",
"github4.png": "github4-copy.png",
"github5.png": "github5-copy.png",
}
copyOps := make([]string, 0, len(copyKeys))
for srcKey, destKey := range copyKeys {
copyOps = append(copyOps, storage.URICopy(srcBucket, srcKey, destBucket, destKey, force))
}
rets, err := bucketManager.Batch(copyOps)
if err != nil {
// 遇到错误
if _, ok := err.(*rpc.ErrorInfo); ok {
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
} else {
fmt.Printf("batch error, %s", err)
}
} else {
// 完全成功
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
fmt.Printf("%v\n", ret.Data)
}
}
}

View File

@@ -0,0 +1,63 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
bucketManager := storage.NewBucketManager(mac, &cfg)
//每个batch的操作数量不可以超过1000个如果总数量超过1000需要分批发送
keys := []string{
"github1.png",
"github2.png",
"github3.png",
"github4.png",
"github5.png",
}
deleteOps := make([]string, 0, len(keys))
for _, key := range keys {
deleteOps = append(deleteOps, storage.URIDelete(bucket, key))
}
rets, err := bucketManager.Batch(deleteOps)
if err != nil {
// 遇到错误
if _, ok := err.(*rpc.ErrorInfo); ok {
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
} else {
fmt.Printf("batch error, %s", err)
}
} else {
// 完全成功
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
}
}
}

View File

@@ -0,0 +1,66 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
bucketManager := storage.NewBucketManager(mac, &cfg)
//每个batch的操作数量不可以超过1000个如果总数量超过1000需要分批发送
expireKeys := map[string]int{
"github1.png": 7,
"github2.png": 8,
"github3.png": 9,
"github4.png": 10,
"github5.png": 11,
}
expireOps := make([]string, 0, len(expireKeys))
for key, expire := range expireKeys {
expireOps = append(expireOps, storage.URIDeleteAfterDays(bucket, key, expire))
}
rets, err := bucketManager.Batch(expireOps)
if err != nil {
// 遇到错误
if _, ok := err.(*rpc.ErrorInfo); ok {
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
} else {
fmt.Printf("batch error, %s", err)
}
} else {
// 完全成功
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
}
}

View File

@@ -0,0 +1,67 @@
package main
import (
"fmt"
"os"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"github.com/qiniu/x/rpc.v7"
)
var (
accessKey = os.Getenv("QINIU_ACCESS_KEY")
secretKey = os.Getenv("QINIU_SECRET_KEY")
bucket = os.Getenv("QINIU_TEST_BUCKET")
)
func main() {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
// 是否使用https域名进行资源管理
UseHTTPS: false,
}
// 指定空间所在的区域,如果不指定将自动探测
// 如果没有特殊需求,默认不需要指定
//cfg.Zone=&storage.ZoneHuabei
bucketManager := storage.NewBucketManager(mac, &cfg)
//每个batch的操作数量不可以超过1000个如果总数量超过1000需要分批发送
srcBucket := bucket
destBucket := bucket
force := true
moveKeys := map[string]string{
"github1.png": "github1-move.png",
"github2.png": "github2-move.png",
"github3.png": "github3-move.png",
"github4.png": "github4-move.png",
"github5.png": "github5-move.png",
}
moveOps := make([]string, 0, len(moveKeys))
for srcKey, destKey := range moveKeys {
moveOps = append(moveOps, storage.URIMove(srcBucket, srcKey, destBucket, destKey, force))
}
rets, err := bucketManager.Batch(moveOps)
if err != nil {
// 遇到错误
if _, ok := err.(*rpc.ErrorInfo); ok {
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
if ret.Code != 200 {
fmt.Printf("%s\n", ret.Data.Error)
}
}
} else {
fmt.Printf("batch error, %s", err)
}
} else {
// 完全成功
for _, ret := range rets {
// 200 为成功
fmt.Printf("%d\n", ret.Code)
fmt.Printf("%v\n", ret.Data)
}
}
}

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