Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/interface/main/upload/cmd:all-srcs",
"//app/interface/main/upload/conf:all-srcs",
"//app/interface/main/upload/dao:all-srcs",
"//app/interface/main/upload/http:all-srcs",
"//app/interface/main/upload/model:all-srcs",
"//app/interface/main/upload/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,80 @@
upload 上传服务
### V1.5.8
> 1. 修复limit rate nil
### V1.5.7
> 1. Content-Type支持自定义指定
### V1.5.6
> 1. 支持csrf在multipart-form中传递
### V1.5.5
> 1. fix binding panic and enable antispam
### V1.5.4
> 1. watermark 降级
### V1.5.3
> 1. 允许url中出现指定dir
> 2. 针对dir新增content-type白名单限制
### V1.5.2
> 1. 允许内网接口指定文件名上传
### V1.5.1
> 1. 去除内网接口限流
### V1.5.0
> 1. 新增web端上传接口
### V1.3.8
> 1. 修复rate limit bug
### V1.3.7
> 1. 新增内网上传接口
### V1.3.6
> 兼容基础库
> 修复返回 json
### V1.3.5
> 修复bind
> 增加第三方上传日志
### V1.3.4
> 载入 ecode
> 修复bind失败返回值
### V1.3.3
> 增加app端上传api
### V1.3.2
> fix外网上传bug
### V1.3.1
> location update
### V1.3.0
> 生成水印logo
> 迁移目录
> 迁移bm
### V1.2.1
> 上传mimettype从header中获取
### V1.2.0
> 添加水印接口
### V1.1.2
> 去掉 ecode nologin
### V1.1.1
> 去掉水印功能移动到thumbnail-service中
### V1.1.0
> 移动到interface下
### V1.0.0
> 1、添加水印支持
> 2、合进大仓库

View File

@@ -0,0 +1,12 @@
# Owner
liangkai
zhapuyu
# Author
guhao
zhuangzhewei
wangweizhen
# Reviewer
liangkai
zhapuyu

View File

@@ -0,0 +1,20 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- guhao
- liangkai
- wangweizhen
- zhapuyu
- zhuangzhewei
labels:
- interface
- interface/main/upload
- main
options:
no_parent_owners: true
reviewers:
- guhao
- liangkai
- wangweizhen
- zhapuyu
- zhuangzhewei

View File

@@ -0,0 +1,44 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["upload-test.toml"],
importpath = "go-common/app/interface/main/upload/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/upload/conf:go_default_library",
"//app/interface/main/upload/http:go_default_library",
"//app/interface/main/upload/service:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/queue/databus/report:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,53 @@
package main
import (
"flag"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/interface/main/upload/conf"
"go-common/app/interface/main/upload/http"
"go-common/app/interface/main/upload/service"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/queue/databus/report"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
ecode.Init(conf.Conf.Ecode)
// init log
log.Init(conf.Conf.XLog)
defer log.Close()
// service init
s := service.New(conf.Conf)
http.Init(conf.Conf, s)
report.InitUser(nil)
log.Info("bfs-upload-interface start!")
// init signal
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("upload get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
time.Sleep(1 * time.Second)
log.Info("upload exit")
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,85 @@
# toml config
[bm]
addr = "0.0.0.0:6831"
timeout = "15s"
[orm]
dsn = "test:test@tcp(172.16.33.205:3308)/bfs?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 10
idle = 10
idleTimeout = "4h"
[xlog]
dir = "/data/log/bfs_upload/"
#[xlog.elk]
#project = "upload"
#addr = "172.18.20.17:8520"
#chanSize = 10240
[bfs]
BfsUrl = "uat-bfs.bilibili.co"
WaterMarkUrl = "http://172.18.33.121:8090/imageserver/watermark/gen"
ImageGenURL = "http://172.18.33.121:8090/imageserver/image/gen"
TimeOut = "10s"
WmTimeOut = "10s"
ImageGenTimeOut = "10s"
[antispam]
Switch = true
Second = 2
N = 1
Hour = 1
M = 100
[antispam.redis]
Name = "bfs-upload"
Proto = "tcp"
Addr = "172.18.33.60:6931"
Auth = ""
Active = 10
Idle = 10
DialTimeout = "10s"
ReadTimeout = "10s"
WriteTimeout = "10s"
IdleTimeout = "10s"
[BfsBucket]
Bucket = "active"
Key = "fb8937a9ccd9b987"
Sercet = "1b05f83578281f293a5b4dc7305fb2"
[[Auths]]
AppKey = "17760953ff"
AppSercet = "d5563249e1a5105d5eb3"
[[Auths]]
AppKey = "pushflow"
AppSercet = "ac78fe240a217b30f75e"
[Auths.BfsBucket]
Bucket = "live"
Key = "b4cfeeadca80f6f5"
Sercet = "b6e9e018302b96bc089a4c23046d16"
[[Auths]]
AppKey = "creative"
AppSercet = "ec396e082dfec9638e3f"
[Auths.BfsBucket]
Bucket = "archive"
Key = "8d4e593ba7555502"
Sercet = "0bdbd4c7caeeddf587c3c4daec0475"
[[Auths]]
AppKey = "bigfun"
AppSercet = "935fb994018b0ceed6fd"
[Auths.BfsBucket]
Bucket = "bigfun"
Key = "710020da99a6f606"
Sercet = "e04327de9cf3e3893194959061995b"
[AuthInter]
DisableCSRF = true
[AuthInter.Identify]
Dial = "500ms"
Timeout = "5s"
[AuthOut]
DisableCSRF = false
[AuthOut.Identify]
Dial = "500ms"
Timeout = "1s"

View File

@@ -0,0 +1,41 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/interface/main/upload/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/upload/http/antispam:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/orm:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/auth:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,131 @@
package conf
import (
"errors"
"flag"
"go-common/app/interface/main/upload/http/antispam"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/orm"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/auth"
"go-common/library/net/http/blademaster/middleware/verify"
xtime "go-common/library/time"
"github.com/BurntSushi/toml"
)
var (
confPath string
client *conf.Client
// Conf conf
Conf = &Config{}
)
// Config config
type Config struct {
XLog *log.Config
// BM
BM *bm.ServerConfig
// ecode
Ecode *ecode.Config
// orm
ORM *orm.Config
// bfs
Bfs *Bfs
BfsBucket *BfsBucket
Auths []*Auth
// Antispam redis
Antispam *antispam.Config
// AuthN
AuthInter *auth.Config
// VerifyN
Verify *verify.Config
// AuthN outside
AuthOut *auth.Config
}
// Bfs .
type Bfs struct {
BfsURL string
WaterMarkURL string
ImageGenURL string
TimeOut xtime.Duration
WmTimeOut xtime.Duration
ImageGenTimeOut xtime.Duration
}
// BfsBucket .
type BfsBucket struct {
Bucket string
Key string
Sercet string
}
// Auth .
type Auth struct {
AppKey string
AppSercet string
BfsBucket *BfsBucket
}
// Antispam .
type Antispam struct {
Redis *redis.Config
Switch bool
Second int
N int
Hour int
M int
}
func init() {
flag.StringVar(&confPath, "conf", "", "config path")
}
// Init init conf
func Init() (err error) {
if confPath != "" {
_, err = toml.DecodeFile(confPath, &Conf)
return
}
err = remote()
return
}
func remote() (err error) {
if client, err = conf.New(); err != nil {
return
}
if err = load(); err != nil {
return
}
go func() {
for range client.Event() {
log.Info("config reload")
if load() != nil {
log.Error("config reload error (%v)", err)
}
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Toml2(); !ok {
return errors.New("load config center error")
}
if _, err = toml.Decode(s, &tmpConf); err != nil {
return errors.New("could not decode config")
}
*Conf = *tmpConf
return
}

View File

@@ -0,0 +1,57 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"bfs_test.go",
"dao_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/upload/conf:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"bfs.go",
"bucket.go",
"dao.go",
],
importpath = "go-common/app/interface/main/upload/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/upload/conf:go_default_library",
"//app/interface/main/upload/model:go_default_library",
"//library/database/orm:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/jinzhu/gorm:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,224 @@
package dao
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"go-common/app/interface/main/upload/conf"
"go-common/library/ecode"
"go-common/library/log"
)
// Bfs .
type Bfs struct {
bfsURL string
waterMarkURL string
imageGenURL string
client *http.Client
wmClient *http.Client
imageGenClient *http.Client
}
// NewBfs .
func NewBfs(c *conf.Config) *Bfs {
return &Bfs{
bfsURL: c.Bfs.BfsURL,
waterMarkURL: c.Bfs.WaterMarkURL,
imageGenURL: c.Bfs.ImageGenURL,
client: &http.Client{
Timeout: time.Duration(c.Bfs.TimeOut),
},
wmClient: &http.Client{
Timeout: time.Duration(c.Bfs.WmTimeOut),
},
imageGenClient: &http.Client{
Timeout: time.Duration(c.Bfs.ImageGenTimeOut),
},
}
}
// GenImage gen a image.
func (b *Bfs) GenImage(ctx context.Context, wmKey, wmText string, distance int, vertical bool) (res []byte, height, width int, hasher string, err error) {
var (
params = url.Values{}
resp *http.Response
)
params.Set("wm_key", wmKey)
params.Set("wm_text", wmText)
params.Set("distance", strconv.Itoa(distance))
params.Set("vertical", strconv.FormatBool(vertical))
if resp, err = b.imageGenClient.PostForm(b.imageGenURL, params); err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Error("bfs.genImage.status(%v)", resp.StatusCode)
}
switch resp.StatusCode {
case http.StatusOK:
case http.StatusBadRequest:
err = ecode.RequestErr
return
case http.StatusMethodNotAllowed:
err = ecode.MethodNotAllowed
return
case http.StatusInternalServerError:
err = ecode.ServerErr
return
default:
err = ecode.ServerErr
return
}
if height, err = strconv.Atoi(resp.Header.Get("O-Height")); err != nil {
log.Error("bfs.genImage.O-Height,err(%v)", err)
return
}
if width, err = strconv.Atoi(resp.Header.Get("O-Width")); err != nil {
log.Error("bfs.genImage.O-Width,err(%v)", err)
return
}
hasher = resp.Header.Get("Md5")
res, err = ioutil.ReadAll(resp.Body)
return
}
// Watermark generate watermark image by key and text.
func (b *Bfs) Watermark(ctx context.Context, data []byte, contentType, wmKey, wmText string, paddingX, paddingY int, wmScale float64) (res []byte, err error) {
var (
resp *http.Response
bw io.Writer
req *http.Request
ext string
)
buf := new(bytes.Buffer)
w := multipart.NewWriter(buf)
if bw, err = w.CreateFormFile("file", "1.jpg"); err != nil {
return
}
if _, err = bw.Write(data); err != nil {
return
}
w.WriteField("wm_key", wmKey)
w.WriteField("wm_text", wmText)
w.WriteField("padding_x", strconv.Itoa(paddingX))
w.WriteField("padding_y", strconv.Itoa(paddingY))
w.WriteField("wm_scale", strconv.FormatFloat(wmScale, 'f', 2, 64))
if ext = path.Base(contentType); ext == "jpeg" {
ext = "jpg"
}
w.WriteField("ext", fmt.Sprintf(".%s", ext))
if err = w.Close(); err != nil {
return
}
if req, err = http.NewRequest(http.MethodPost, b.waterMarkURL, buf); err != nil {
return
}
req.Header.Set("Content-Type", w.FormDataContentType())
if resp, err = b.wmClient.Do(req); err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Error("bfs.waterMark.status(%v)", resp.StatusCode)
err = ecode.ServerErr
return
}
res, err = ioutil.ReadAll(resp.Body)
return
}
// Upload upload file to bfs.
func (b *Bfs) Upload(ctx context.Context, key, secret, contentType, bucket, dir, filename string, data []byte) (location, etag string, err error) {
dir = strings.Trim(dir, "/")
var (
buf = new(bytes.Buffer)
fname = path.Join(dir, filename)
)
// 多级目录上传request url必须以 / 结尾
if filename == "" && dir != "" {
fname = fname + "/"
}
reqURL := fmt.Sprintf("http://%s/bfs/%s/%s", b.bfsURL, bucket, fname)
if _, err = buf.Write(data); err != nil {
log.Error("Upload.buf.Write.error(%v)", err)
err = ecode.ServerErr
return
}
req, err := http.NewRequest(http.MethodPut, reqURL, buf)
if err != nil {
err = ecode.ServerErr
return
}
auth := b.authorize(key, secret, http.MethodPut, bucket, fname, time.Now().Unix())
req.Header.Add("Content-Type", contentType)
req.Header.Add("Authorization", auth)
resp, err := b.client.Do(req)
if err != nil {
log.Error("Upload.client.Do.error(%v)", err)
err = ecode.ServerErr
return
}
if resp.StatusCode != http.StatusOK {
log.Error("bucket:%s,filename:%s,response code:%+v", bucket, filename, resp.StatusCode)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
case http.StatusBadRequest:
err = ecode.RequestErr
return
case http.StatusUnauthorized:
// 验证不通过
err = ecode.BfsUploadAuthErr
return
case http.StatusRequestEntityTooLarge:
err = ecode.FileTooLarge
return
case http.StatusNotFound:
err = ecode.NothingFound
return
case http.StatusMethodNotAllowed:
err = ecode.MethodNotAllowed
return
case http.StatusServiceUnavailable:
err = ecode.BfsUploadServiceUnavailable
return
case http.StatusInternalServerError:
err = ecode.ServerErr
return
default:
err = ecode.BfsUploadStatusErr
return
}
code, err := strconv.Atoi(resp.Header.Get("Code"))
if err != nil || code != 200 {
log.Error("bucket:%s,dir:%s,filename:%s response.Header.Code(%s) error(%v)", bucket, dir, filename, resp.Header.Get("Code"), err)
err = ecode.BfsUploadCodeErr
return
}
location = resp.Header.Get("location")
etag = resp.Header.Get("etag")
return
}
// authorize generate authorization
func (b *Bfs) authorize(key, secret, method, bucket, fileName string, expire int64) string {
content := fmt.Sprintf("%s\n%s\n%s\n%d\n", method, bucket, fileName, expire)
mac := hmac.New(sha1.New, []byte(secret))
mac.Write([]byte(content))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return fmt.Sprintf("%s:%s:%d", key, signature, expire)
}

View File

@@ -0,0 +1,84 @@
package dao
import (
"context"
"testing"
"time"
"go-common/app/interface/main/upload/conf"
xtime "go-common/library/time"
. "github.com/smartystreets/goconvey/convey"
)
func TestNewBfs(t *testing.T) {
Convey("new bfs instance", t, func() {
b := NewBfs(&conf.Config{
Bfs: &conf.Bfs{
BfsURL: "uat-bfs.bilibili.co",
WaterMarkURL: "http://i0.hdslb.com/imageserver/watermark/gen",
TimeOut: xtime.Duration(time.Second * 5),
WmTimeOut: xtime.Duration(time.Second * 5),
},
})
So(b, ShouldNotBeNil)
})
}
func TestGenImage(t *testing.T) {
Convey("create watermark image", t, func() {
image, height, width, hasher, err := b.GenImage(context.TODO(), "comic", "hello world", 2, true)
So(err, ShouldBeNil)
So(image, ShouldNotBeEmpty)
So(height, ShouldNotEqual, 0)
So(width, ShouldNotEqual, 0)
So(hasher, ShouldNotEqual, "")
})
}
func TestWatermark(t *testing.T) {
Convey("do watermark action", t, func() {
image, err := b.Watermark(context.TODO(), testData, "image/png", "comic", "hello", 0, 0, 0)
So(err, ShouldBeNil)
So(image, ShouldNotBeEmpty)
})
}
func TestUpload(t *testing.T) {
Convey("upload", t, func() {
var (
dir = "dir1/"
filename = "1111.jpg"
)
location, _, err := b.Upload(context.Background(), "1b24a3d8560d2213", "415aaa6ff53659dabf8a2de394025a", "image/jpg", "static", dir, filename, testData)
So(err, ShouldBeNil)
So(location, ShouldNotBeEmpty)
})
Convey("upload", t, func() {
var (
dir = "dir1/"
filename = ""
)
location, _, err := b.Upload(context.Background(), "1b24a3d8560d2213", "415aaa6ff53659dabf8a2de394025a", "image/jpg", "static", dir, filename, testData)
So(err, ShouldBeNil)
So(location, ShouldNotBeEmpty)
})
Convey("upload", t, func() {
var (
dir = ""
filename = "1111.jpg"
)
location, _, err := b.Upload(context.Background(), "1b24a3d8560d2213", "415aaa6ff53659dabf8a2de394025a", "image/jpg", "static", dir, filename, testData)
So(err, ShouldBeNil)
So(location, ShouldNotBeEmpty)
})
Convey("upload", t, func() {
var (
dir = ""
filename = ""
)
location, _, err := b.Upload(context.Background(), "1b24a3d8560d2213", "415aaa6ff53659dabf8a2de394025a", "image/jpg", "static", dir, filename, testData)
So(err, ShouldBeNil)
So(location, ShouldNotBeEmpty)
})
}

View File

@@ -0,0 +1,68 @@
package dao
import (
"encoding/json"
"strings"
"go-common/app/interface/main/upload/model"
"go-common/library/log"
)
// Buckets all bucket info from database.
func (d *Dao) Buckets() (bucketMap map[string]*model.Bucket, err error) {
var (
buckets []*model.Bucket
limitMap map[string]map[string]*model.DirConfig
)
if err = d.orm.Table("bucket").Find(&buckets).Error; err != nil {
log.Error("orm.Table(bucket) error(%v)", err)
return
}
if limitMap, err = d.dirLimits(); err != nil {
return
}
bucketMap = make(map[string]*model.Bucket)
for _, b := range buckets {
v, ok := limitMap[b.Name]
if ok {
b.DirLimit = v
}
bucketMap[b.Name] = b
}
return
}
// dirLimits directory limit from database.
func (d *Dao) dirLimits() (limitMap map[string]map[string]*model.DirConfig, err error) {
limits := make([]*model.DirLimit, 0)
if err = d.orm.Table("dir_limit").Find(&limits).Error; err != nil {
return
}
limitMap = make(map[string]map[string]*model.DirConfig)
for _, l := range limits {
var (
pic model.DirPicConfig
rate model.DirRateConfig
)
if err = json.Unmarshal([]byte(l.DirPicConfig), &pic); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", l.DirPicConfig, err)
err = nil
continue
}
if pic.AllowType != "" {
pic.AllowTypeSlice = strings.Split(pic.AllowType, ",")
}
if err = json.Unmarshal([]byte(l.DirRateConfig), &rate); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", l.DirRateConfig, err)
err = nil
continue
}
if _, ok := limitMap[l.BucketName]; !ok {
limitMap[l.BucketName] = make(map[string]*model.DirConfig)
}
// NOTE empty dir is also in limit map
l.Dir = strings.Trim(l.Dir, "/")
limitMap[l.BucketName][l.Dir] = &model.DirConfig{Pic: pic, Rate: rate}
}
return
}

View File

@@ -0,0 +1,27 @@
package dao
import (
"context"
"go-common/app/interface/main/upload/conf"
"go-common/library/database/orm"
"github.com/jinzhu/gorm"
)
// Dao dao struct
type Dao struct {
orm *gorm.DB
}
// NewDao new a dao instance.
func NewDao(c *conf.Config) *Dao {
return &Dao{
orm: orm.NewMySQL(c.ORM),
}
}
// Ping ping database.
func (d *Dao) Ping(c context.Context) error {
return nil
}

View File

@@ -0,0 +1,50 @@
package dao
import (
"io/ioutil"
"net/http"
"os"
"sync"
"testing"
"time"
"go-common/app/interface/main/upload/conf"
xtime "go-common/library/time"
)
var (
b *Bfs
testData []byte
once sync.Once
)
func TestMain(m *testing.M) {
initData()
once.Do(initBFS)
os.Exit(m.Run())
}
func initBFS() {
b = NewBfs(&conf.Config{
Bfs: &conf.Bfs{
BfsURL: "uat-bfs.bilibili.co",
WaterMarkURL: "http://172.18.33.121:8090/imageserver/watermark/gen",
ImageGenURL: "http://172.18.33.121:8090/imageserver/image/gen",
TimeOut: xtime.Duration(time.Second * 5),
WmTimeOut: xtime.Duration(time.Second * 5),
ImageGenTimeOut: xtime.Duration(time.Second * 5),
},
})
}
func initData() {
client := &http.Client{}
resp, err := client.Get("http://uat-i0.hdslb.com/bfs/archive/fc7cd08beb29f93c596426557cf1aa11a08e9730.jpg")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if testData, err = ioutil.ReadAll(resp.Body); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,47 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"http.go",
"record.go",
],
importpath = "go-common/app/interface/main/upload/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/upload/conf:go_default_library",
"//app/interface/main/upload/http/antispam:go_default_library",
"//app/interface/main/upload/model:go_default_library",
"//app/interface/main/upload/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/binding:go_default_library",
"//library/net/http/blademaster/middleware/auth:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/http/blademaster/render:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/interface/main/upload/http/antispam:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["antispam.go"],
importpath = "go-common/app/interface/main/upload/http/antispam",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/upload/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/binding:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["antispam_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/upload/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/container/pool:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/time:go_default_library",
],
)

View File

@@ -0,0 +1,159 @@
package antispam
import (
"fmt"
"strings"
"time"
"go-common/app/interface/main/upload/model"
"go-common/library/cache/redis"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/binding"
)
const (
_prefixRate = "r_%d_%s_%d"
_prefixTotal = "t_%d_%s_%d"
)
// Antispam is a antispam instance.
type Antispam struct {
redis *redis.Pool
limitFunc func(bucket, dir string) (model.DirRateConfig, bool)
conf *Config
}
// Config antispam config.
// On bool // switch on/off
// Second int // every N second allow N requests.
// N int // one unit allow N requests.
// Hour int // every N hour allow M requests.
// M int // one winodw allow M requests.
type Config struct {
On bool // switch on/off
Second int // every N second allow N requests.
N int // one unit allow N requests.
Hour int // every N hour allow M requests.
M int // one winodw allow M requests.
Redis *redis.Config
}
// New new a antispam service.
func New(c *Config, l func(bucket, dir string) (model.DirRateConfig, bool)) (s *Antispam) {
if c == nil {
panic("antispam config nil")
}
s = &Antispam{
limitFunc: l,
redis: redis.NewPool(c.Redis),
}
s.conf = c
return s
}
// NativeRate limit user + path second level
func (s *Antispam) NativeRate(c *bm.Context, path string, mid interface{}) (err error) {
curSecond := int(time.Now().Unix())
burst := curSecond - curSecond%s.conf.Second
key := rateKey(mid.(int64), path, burst)
return s.antispam(c, key, s.conf.Second, s.conf.N)
}
// Rate antispam by user + bucket + dir.
func (s *Antispam) Rate(c *bm.Context) (err error) {
mid, ok := c.Get("mid")
if !ok {
return
}
ap := new(struct {
Bucket string `form:"bucket" json:"bucket"`
Dir string `form:"dir" json:"dir"`
})
if err = c.BindWith(ap, binding.FormMultipart); err != nil {
return s.NativeRate(c, c.Request.URL.Path, mid)
}
if ap.Bucket == "" || ap.Dir == "" { //not need dir limit
return s.NativeRate(c, c.Request.URL.Path, mid)
}
limit, ok := s.limitFunc(ap.Bucket, ap.Dir)
if !ok {
return s.NativeRate(c, c.Request.URL.Path, mid)
}
if limit.SecondQPS == 0 || limit.CountQPS == 0 {
return s.NativeRate(c, c.Request.URL.Path, mid)
}
path := strings.Join([]string{ap.Bucket, ap.Dir}, "_")
curSecond := int(time.Now().Unix())
burst := curSecond - curSecond%limit.SecondQPS
key := rateKey(mid.(int64), path, burst)
return s.antispam(c, key, limit.SecondQPS, limit.CountQPS)
}
func totalKey(mid int64, path string, burst int) string {
return fmt.Sprintf(_prefixTotal, mid, path, burst)
}
// Total antispam by user + path hour level
func (s *Antispam) Total(c *bm.Context, hour, count int) (err error) {
second := hour * 3600
mid, ok := c.Get("mid")
if !ok {
return
}
curHour := int(time.Now().Unix() / 3600)
burst := curHour - curHour%hour
key := totalKey(mid.(int64), c.Request.URL.Path, burst)
return s.antispam(c, key, second, count)
}
func (s *Antispam) antispam(c *bm.Context, key string, interval, count int) (err error) {
conn := s.redis.Get(c)
defer conn.Close()
cur, err := redis.Int(conn.Do("GET", key))
if err != nil && err != redis.ErrNil {
err = nil
return
}
if cur >= count {
err = ecode.LimitExceed
return
}
err = nil
conn.Send("INCR", key)
conn.Send("EXPIRE", key, interval)
if err1 := conn.Flush(); err1 != nil {
return
}
for i := 0; i < 2; i++ {
if _, err1 := conn.Receive(); err1 != nil {
return
}
}
return
}
func rateKey(mid int64, path string, burst int) string {
return fmt.Sprintf(_prefixRate, mid, path, burst)
}
func (s *Antispam) ServeHTTP(ctx *bm.Context) {
// user + bucket + dir.
if err := s.Rate(ctx); err != nil {
ctx.JSON(nil, ecode.ServiceUnavailable)
ctx.Abort()
return
}
// user + path
if err := s.Total(ctx, s.conf.Hour, s.conf.M); err != nil {
ctx.JSON(nil, ecode.ServiceUnavailable)
ctx.Abort()
return
}
}
// Handler is antispam handle.
func (s *Antispam) Handler() bm.HandlerFunc {
return s.ServeHTTP
}

View File

@@ -0,0 +1,98 @@
package antispam
import (
"context"
"io/ioutil"
"math/rand"
"net/http"
"strconv"
"testing"
"time"
"go-common/app/interface/main/upload/model"
"go-common/library/cache/redis"
"go-common/library/container/pool"
bm "go-common/library/net/http/blademaster"
xtime "go-common/library/time"
)
func TestAntiSpamHandler(t *testing.T) {
anti := New(
&Config{
On: true,
Second: 1,
N: 1,
Hour: 1,
M: 1,
Redis: &redis.Config{
Config: &pool.Config{
Active: 10,
Idle: 10,
IdleTimeout: xtime.Duration(time.Second * 60),
},
Name: "test",
Proto: "tcp",
Addr: "172.16.33.54:6380",
DialTimeout: xtime.Duration(time.Second),
ReadTimeout: xtime.Duration(time.Second),
WriteTimeout: xtime.Duration(time.Second),
}}, GetGetRateLimit)
engine := bm.New()
engine.UseFunc(func(c *bm.Context) {
mid, _ := strconv.ParseInt(c.Request.Form.Get("mid"), 10, 64)
c.Set("mid", mid)
c.Next()
})
engine.Use(anti.Handler())
engine.GET("/antispam", func(c *bm.Context) {
c.String(200, "pass")
})
go engine.Run(":18080")
time.Sleep(time.Millisecond * 50)
mid := rand.Int()
_, content, err := httpGet("http://127.0.0.1:18080/antispam?mid=" + strconv.Itoa(mid) + "&bucket=a&dir=b")
if err != nil {
t.Logf("http get failed, err:=%v", err)
t.FailNow()
}
if string(content) != "pass" {
t.Logf("request should block by limiter, but passed")
t.FailNow()
}
_, content, err = httpGet("http://127.0.0.1:18080/antispam?mid=" + strconv.Itoa(mid) + "&bucket=a&dir=b")
if err != nil {
t.Logf("http get failed, err:=%v", err)
t.FailNow()
}
if string(content) == "pass" {
t.Logf("request should block by limiter, but passed")
t.FailNow()
}
engine.Server().Shutdown(context.TODO())
}
func httpGet(url string) (code int, content []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
content, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
code = resp.StatusCode
return
}
func GetGetRateLimit(bucket, dir string) (model.DirRateConfig, bool) {
return model.DirRateConfig{
SecondQPS: 1,
CountQPS: 1,
}, true
}

View File

@@ -0,0 +1,68 @@
package http
import (
"net/http"
"go-common/app/interface/main/upload/conf"
xanti "go-common/app/interface/main/upload/http/antispam"
"go-common/app/interface/main/upload/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/auth"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
uploadSvr *service.Service
authInterSvr *auth.Auth
authSvr *auth.Auth
verifySvr *verify.Verify
anti *xanti.Antispam
)
// Init init http
func Init(c *conf.Config, s *service.Service) {
initService(c, s)
engine := bm.DefaultServer(c.BM)
initRouter(engine)
// init Outer serve
if err := engine.Start(); err != nil {
log.Error("xhttp.Serve error(%v)", err)
panic(err)
}
}
func initService(c *conf.Config, s *service.Service) {
uploadSvr = s
authInterSvr = auth.New(c.AuthInter)
authSvr = auth.New(c.AuthOut)
verifySvr = verify.New(nil)
anti = xanti.New(c.Antispam, s.GetRateLimit) //mid+dir 限流
}
func initRouter(e *bm.Engine) {
e.Ping(ping)
uploadInternal := e.Group("/x/internal")
{
uploadInternal.POST("/upload", verifySvr.Verify, internalUpload)
uploadInternal.POST("/upload/image", authInterSvr.User, anti.Handler(), internalUploadImage)
uploadInternal.POST("/upload/admin/image", verifySvr.Verify, anti.Handler(), internalUploadAdminImage)
uploadInternal.POST("/image/gen", verifySvr.Verify, anti.Handler(), genImageUpload)
}
upload := e.Group("/x/upload")
{
upload.POST("/image", uploadImagePublic)
upload.POST("/app/image", authSvr.UserMobile, anti.Handler(), uploadMobileImage)
upload.POST("/web/image", authSvr.UserWeb, anti.Handler(), uploadWebImage)
}
}
// ping check server ok.
func ping(c *bm.Context) {
var err error
if err = uploadSvr.Ping(c); err != nil {
log.Error("upload service ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}

View File

@@ -0,0 +1,250 @@
package http
import (
"bytes"
"io"
"net/http"
"strconv"
"go-common/app/interface/main/upload/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/binding"
"go-common/library/net/http/blademaster/render"
)
const (
_defaultDistance = 1
)
// genImageUpload .
func genImageUpload(c *bm.Context) {
params := c.Request.Form
uploadKey := params.Get("upload_key")
wmKey := params.Get("wm_key")
wmText := params.Get("wm_text")
if len(wmText) > 20 {
c.JSON(nil, ecode.RequestErr)
return
}
distance, err := strconv.Atoi(params.Get("distance"))
if err != nil {
distance = _defaultDistance
}
vertical, err := strconv.ParseBool(params.Get("wm_vertical"))
if err != nil {
vertical = true
}
c.JSON(uploadSvr.GenImageUpload(c, uploadKey, wmKey, wmText, distance, vertical))
}
// uploadImagePublic .
func uploadImagePublic(c *bm.Context) {
if err := c.Request.ParseMultipartForm(model.MaxUploadSize); err != nil {
c.JSON(nil, ecode.BfsUploadFileTooLarge)
return
}
params := c.Request.Form
uploadKey := params.Get("upload_key")
uploadToken := params.Get("upload_token")
file, header, err := c.Request.FormFile("file")
if err != nil {
log.Error("upload.UploadImage.file.illegal,err:(%v)", err.Error())
c.JSON(nil, ecode.RequestErr)
return
}
defer file.Close()
buf := new(bytes.Buffer)
if _, err = io.Copy(buf, file); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
result, err := uploadSvr.Upload(c, uploadKey, uploadToken, header.Header.Get("Content-Type"), buf.Bytes())
if err != nil {
c.JSON(nil, err)
return
}
c.JSON(result, nil)
}
// internalUpload upload by key and sign.
func internalUpload(c *bm.Context) {
var (
err error
mid int64
)
up := new(model.UploadParam)
if err = c.BindWith(up, binding.FormMultipart); err != nil {
return
}
up.WMInit()
if midInter, ok := c.Get("mid"); ok {
mid = midInter.(int64)
}
file, _, err := c.Request.FormFile("file")
if err != nil {
log.Error("upload.UploadImage.file.illegal,err::%v", err.Error())
c.JSON(nil, ecode.FileNotExists)
return
}
defer file.Close()
buf := new(bytes.Buffer)
if _, err = io.Copy(buf, file); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(uploadSvr.UploadRecord(c, model.UploadInternal, mid, up, buf.Bytes()))
}
// internalUploadImage .
func internalUploadImage(c *bm.Context) {
var (
err error
mid int64
)
up := new(model.UploadParam)
if err = c.BindWith(up, binding.FormMultipart); err != nil {
return
}
up.WMInit()
if midInter, ok := c.Get("mid"); ok {
mid = midInter.(int64)
}
file, _, err := c.Request.FormFile("file")
if err != nil {
log.Error("upload.UploadImage.file.illegal,err::%v", err.Error())
c.JSON(nil, ecode.FileNotExists)
return
}
defer file.Close()
buf := new(bytes.Buffer)
if _, err = io.Copy(buf, file); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
result, err := uploadSvr.UploadRecord(c, model.UploadInternal, mid, up, buf.Bytes())
if err != nil {
log.Error("uploadSvr.UploadRecord(%d,%+v) error(%v)", mid, up, err)
c.JSON(nil, err)
return
}
c.JSON(result, nil)
}
// internalUploadAdminImage .
func internalUploadAdminImage(c *bm.Context) {
var err error
up := new(model.UploadParam)
if err = c.BindWith(up, binding.FormMultipart); err != nil {
return
}
up.WMInit()
file, _, err := c.Request.FormFile("file")
if err != nil {
log.Error("upload.UploadImage.file.illegal,err::%v", err.Error())
c.JSON(nil, ecode.RequestErr)
return
}
defer file.Close()
buf := new(bytes.Buffer)
if _, err = io.Copy(buf, file); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
result, err := uploadSvr.UploadAdminRecord(c, model.UploadInternalAdmin, up, buf.Bytes())
if err != nil {
log.Error("uploadSrv.Upload(%v,%v,%v) error(%v)", up.Bucket, up.FileName, err)
c.JSON(nil, err)
return
}
c.JSON(result, nil)
}
// uploadMobileImage .
func uploadMobileImage(c *bm.Context) {
var (
err error
mid int64
)
up := new(model.UploadParam)
if err = c.BindWith(up, binding.FormMultipart); err != nil {
return
}
up.WMInit()
if midInter, ok := c.Get("mid"); ok {
mid = midInter.(int64)
}
if mid <= 0 {
c.Render(http.StatusOK, render.JSON{
Code: ecode.UserNotExist.Code(),
Message: "invalid or not exist mid",
Data: nil,
})
return
}
file, _, err := c.Request.FormFile("file")
if err != nil {
log.Error("upload.UploadMobileImage.file.illegal,err::%v", err.Error())
c.JSON(nil, ecode.FileNotExists)
return
}
defer file.Close()
buf := new(bytes.Buffer)
if _, err = io.Copy(buf, file); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
result, err := uploadSvr.UploadRecord(c, model.UploadApp, mid, up, buf.Bytes())
if err != nil {
log.Error("uploadSrv.UploadRecord(%v,%v,%v) error(%v)", mid, up.Bucket, up.FileName, err)
c.JSON(nil, err)
return
}
log.Info("app/upload param (%+v) result (%+v)", up, result)
c.JSON(result, nil)
}
// uploadWebImage .
func uploadWebImage(c *bm.Context) {
var (
err error
mid int64
)
up := new(model.UploadParam)
if err = c.BindWith(up, binding.FormMultipart); err != nil {
return
}
up.WMInit()
if midInter, ok := c.Get("mid"); ok {
mid = midInter.(int64)
}
if mid <= 0 {
c.Render(http.StatusOK, render.JSON{
Code: ecode.UserNotExist.Code(),
Message: "invalid or not exist mid",
Data: nil,
})
return
}
file, _, err := c.Request.FormFile("file")
if err != nil {
log.Error("upload.UploadWebImage.file.illegal,err::%v", err.Error())
c.JSON(nil, ecode.FileNotExists)
return
}
defer file.Close()
buf := new(bytes.Buffer)
if _, err = io.Copy(buf, file); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
result, err := uploadSvr.UploadRecord(c, model.UploadWeb, mid, up, buf.Bytes())
if err != nil {
log.Error("uploadSrv.UploadRecord(%v,%v,%v) error(%v)", mid, up.Bucket, up.FileName, err)
c.JSON(nil, err)
return
}
log.Info("web/upload param (%+v) result (%+v)", up, result)
c.JSON(result, nil)
}

View File

@@ -0,0 +1,34 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"bucket.go",
"dir.go",
"model.go",
"upload.go",
],
importpath = "go-common/app/interface/main/upload/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//library/time:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,40 @@
package model
import (
"go-common/library/time"
)
// Bucket in accord with bucket table in database
type Bucket struct {
ID int `json:"id" gorm:"column:id"`
Name string `json:"name" gorm:"column:bucket_name"`
Property int `json:"property" gorm:"column:property"`
KeyID string `json:"key_id" gorm:"column:key_id"`
KeySecret string `json:"key_secret" gorm:"column:key_secret"`
PurgeCDN bool `json:"purge_cdn" gorm:"column:purge_cdn"`
CacheControl int `json:"cache_control" gorm:"column:purge_cdn"`
CTime time.Time `json:"ctime" gorm:"column:ctime"`
MTime time.Time `json:"mtime" gorm:"column:mtime"`
DirLimit map[string]*DirConfig `json:"dir_limit" gorm:"-"`
}
// DirLimit in accord with dir_limit table in database
type DirLimit struct {
ID int `json:"id" gorm:"column:id"`
BucketName string `json:"bucket_name" gorm:"column:bucket_name"`
Dir string `json:"dir" gorm:"column:dir"`
DirPicConfig string `json:"dir_pic_config" gorm:"column:config_pic"`
DirRateConfig string `json:"dir_rate_config" gorm:"column:config_rate"`
CTime time.Time `json:"ctime" gorm:"column:ctime"`
MTime time.Time `json:"mtime" gorm:"column:mtime"`
}
// TableName return table name.
func (b Bucket) TableName() string {
return "bucket"
}
// TableName return table name.
func (l DirLimit) TableName() string {
return "dir_limit"
}

View File

@@ -0,0 +1,42 @@
package model
// DirConfig directory config
type DirConfig struct {
Pic DirPicConfig `json:"dir_pic_config"`
Rate DirRateConfig `json:"dir_rate_config"`
}
// DirPicConfig directory picture config
type DirPicConfig struct {
FileSize int `json:"file_size"` //文件大小上限 单位 Byte
MaxPixelWidthSize int `json:"max_pixel_width_size"` //像素宽上限
MinPixelWidthSize int `json:"min_pixel_width_size"` //像素高下限
MaxPixelHeightSize int `json:"max_pixel_height_size"` //像素高上限
MinPixelHeightSize int `json:"min_pixel_height_size"` //像素宽下限
MaxAspectRatio float64 `json:"max_aspect_ratio"` //最大宽高比
MinAspectRatio float64 `json:"min_aspect_ratio"` //最小宽高比
AllowType string `json:"allow_type"` //允许的MIME类型
AllowTypeSlice []string // 允许的MIME类型列表,AllowTypeSlice = strings.Split(AllowType,",")
}
// DirRateConfig directory rate config
type DirRateConfig struct {
// secondQPS 接受 countQPS 个请求
SecondQPS int `json:"second_qps"`
CountQPS int `json:"count_qps"`
}
//{
//    file_size 100                   文件大小上限 单位 Byte
//    max_pixel_width_size 1024       像素宽上限
//    max_pixel_height_size1024       像素高上限
//    min_pixel_width_size 10         像素宽下限
//    min_pixel_height_size10         像素高下限
//    max_aspect_ratio 100            最大宽高比
//    min_aspect_ratio 10             最小宽高比
//}
//{
// max_user_qps 最大用户qps
// max_user_upload_number 每日最大用户上传数量
//}

View File

@@ -0,0 +1,49 @@
package model
const (
_defaultWmPaddingX = 10
_defaultWmPaddingY = 10
_defaultWmScale = float64(1) / 24
)
// Result upload result
type Result struct {
Location string `json:"location"`
Etag string `json:"etag"`
}
// ResultWm watermark result
type ResultWm struct {
Location string `json:"location"`
Md5 string `json:"md5"`
Height int `json:"height"`
Width int `json:"width"`
}
// UploadParam upload params
type UploadParam struct {
Bucket string `form:"bucket" json:"bucket" validate:"required" `
ContentType string `form:"content_type" json:"content_type"`
Dir string `form:"dir" json:"dir"`
FileName string `form:"file_name" json:"file_name"`
WmKey string `form:"wm_key" json:"wm_key"`
WmText string `form:"wm_text" json:"wm_text"`
WmPaddingX int `form:"wm_padding_x" json:"wm_padding_x"`
WmPaddingY int `form:"wm_padding_y" json:"wm_padding_y"`
WmScale float64 `form:"wm_scale" json:"wm_scale"`
}
// WMInit init watermark default value.
func (up *UploadParam) WMInit() {
if up.WmKey != "" || up.WmText != "" {
if up.WmPaddingX < 0 {
up.WmPaddingX = _defaultWmPaddingX
}
if up.WmPaddingY < 0 {
up.WmPaddingY = _defaultWmPaddingY
}
if up.WmScale <= 0 {
up.WmScale = _defaultWmScale
}
}
}

View File

@@ -0,0 +1,36 @@
package model
const (
// MaxUploadSize max upload file size
MaxUploadSize = 20 * 1024 * 1024
)
// UploadActionType report action type
type UploadActionType int
// report action type
const (
UploadInternal UploadActionType = iota + 1 // 内网用户
UploadInternalAdmin // 内网管理员
UploadPublic // 外网公用
UploadApp // 外网 app
UploadWeb // 外网web
)
func (a UploadActionType) String() (s string) {
switch a {
case UploadInternal:
s = "internal_upload"
case UploadInternalAdmin:
s = "internal_admin_upload"
case UploadPublic:
s = "outer_public_upload"
case UploadApp:
s = "outer_app_upload"
case UploadWeb:
s = "outer_web_upload"
default:
s = "undefined_upload"
}
return
}

View File

@@ -0,0 +1,57 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"record_test.go",
"service_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/upload/conf:go_default_library",
"//app/interface/main/upload/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"pixel.go",
"record.go",
"service.go",
],
importpath = "go-common/app/interface/main/upload/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/upload/conf:go_default_library",
"//app/interface/main/upload/dao:go_default_library",
"//app/interface/main/upload/model:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/queue/databus/report:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,18 @@
package service
import (
"image"
"io"
"go-common/library/log"
)
// Pixel get width height
func Pixel(file io.Reader) (width, height int, err error) {
var c image.Config
if c, _, err = image.DecodeConfig(file); err != nil {
log.Error("decode config error", err)
return
}
return c.Width, c.Height, err
}

View File

@@ -0,0 +1,282 @@
package service
import (
"bytes"
"context"
"crypto/sha1"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go-common/app/interface/main/upload/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/queue/databus/report"
)
const (
_genImageContentType = "image/png"
)
// GenImageUpload generate watermark image by text and upload it.
func (s *Service) GenImageUpload(ctx context.Context, uploadKey string, wmKey, wmText string, distance int, vertical bool) (res *model.ResultWm, err error) {
var image []byte
res = new(model.ResultWm)
key, secret, bucket := s.authorizeInfo(uploadKey)
if image, res.Height, res.Width, res.Md5, err = s.bfs.GenImage(ctx, wmKey, wmText, distance, vertical); err != nil {
return
}
res.Location, _, err = s.bfs.Upload(ctx, key, secret, _genImageContentType, bucket, "", "", image)
return
}
// Upload upload by key and secret.
func (s *Service) Upload(ctx context.Context, uploadKey, uploadToken, contentType string, data []byte) (result *model.Result, err error) {
if !s.verify(uploadKey, uploadToken) {
err = ecode.AccessDenied
return
}
key, secret, bucket := s.authorizeInfo(uploadKey)
if contentType == "" {
contentType = http.DetectContentType(data)
}
location, etag, err := s.bfs.Upload(ctx, key, secret, contentType, bucket, "", "", data)
if err != nil {
return
}
result = &model.Result{
Location: location,
Etag: etag,
}
return
}
// authorizeInfo get authorize info by upload key.
func (s *Service) authorizeInfo(uploadKey string) (key, secret, bucket string) {
key = s.c.BfsBucket.Key
secret = s.c.BfsBucket.Sercet
bucket = s.c.BfsBucket.Bucket
for _, a := range s.c.Auths {
if a.AppKey == uploadKey && a.BfsBucket != nil {
key = a.BfsBucket.Key
secret = a.BfsBucket.Sercet
bucket = a.BfsBucket.Bucket
break
}
}
return
}
func (s *Service) verify(key, token string) bool {
var (
expire, now, delta int64
err error
)
for _, auth := range s.c.Auths {
if key == auth.AppKey {
srcs := strings.Split(token, ":")
if len(srcs) != 2 {
log.Error("verify error: len(srcs) != 2")
return false
}
if expire, err = strconv.ParseInt(srcs[1], 10, 64); err != nil {
log.Error("verify error: expire not int (%v)", err)
return false
}
if s.gen(auth.AppKey, auth.AppSercet, expire) != srcs[0] {
log.Error("verify error: s.gen(%s,%s,%d) != srcs[0]:%s", auth.AppKey, auth.AppSercet, expire, srcs[0])
return false
}
now = time.Now().Unix()
// > ±15 min is forbidden
if expire > now {
delta = expire - now
} else {
delta = now - expire
}
if delta > 900 {
log.Error("verify error: delta > 900")
return false
}
return true
}
}
return false
}
func (s *Service) gen(key, sercet string, now int64) string {
sha1 := sha1.New()
sha1.Write([]byte(fmt.Sprintf("i love bilibili %s:%d", sercet, now)))
return hex.EncodeToString(sha1.Sum([]byte("")))
}
// UploadRecord .
func (s *Service) UploadRecord(ctx context.Context, action model.UploadActionType, mid int64, up *model.UploadParam, data []byte) (result *model.Result, err error) {
var (
location string
etag string
b *model.Bucket
ok bool
)
if b, ok = s.bucketCache[up.Bucket]; !ok {
err = ecode.BfsUplaodBucketNotExist
log.Error("read bucket items failed: (%s)", up.Bucket)
return
}
// content-type
if up.ContentType == "" {
up.ContentType = http.DetectContentType(data)
}
// check limit if dir not null
if up.Dir != "" {
up.Dir = strings.Trim(up.Dir, "/")
//todo: dir limit if conf exist
if err = s.dirlimit(up.Bucket, up.Dir, up.ContentType, data); err != nil {
return
}
}
log.Info("upload record params:%+v", up)
if up.WmKey != "" || up.WmText != "" {
var image []byte
if image, err = s.bfs.Watermark(ctx, data, up.ContentType, up.WmKey, up.WmText, up.WmPaddingX, up.WmPaddingY, up.WmScale); err != nil {
log.Error("Upload.Watermark data length(%d) params(%+v) error(%v)", len(data), up, err)
} else {
data = image
}
}
if location, etag, err = s.bfs.Upload(ctx, b.KeyID, b.KeySecret, up.ContentType, up.Bucket, up.Dir, up.FileName, data); err != nil {
return
}
uri, err := url.Parse(location)
if err != nil {
return
}
//todo: add user report
// http://info.bilibili.co/pages/viewpage.action?pageId=8474335
uInfo := &report.UserInfo{
Mid: mid,
Platform: "bfs-upload-interface",
Build: 1,
Business: 61, //bfs-upload
Action: action.String(),
Ctime: time.Now(),
Index: []interface{}{location, up.Bucket, up.Dir, uri.Path}, // path is filename
Content: map[string]interface{}{
"upload_param": up,
},
}
report.User(uInfo)
result = &model.Result{
Location: location,
Etag: etag,
}
return
}
// UploadAdminRecord no dir limit upload method.
func (s *Service) UploadAdminRecord(ctx context.Context, action model.UploadActionType, up *model.UploadParam, data []byte) (result *model.Result, err error) {
var (
location, etag string
b *model.Bucket
ok bool
)
if b, ok = s.bucketCache[up.Bucket]; !ok {
err = ecode.BfsUplaodBucketNotExist
log.Error("read bucket items failed: (%s)", up.Bucket)
return
}
if up.ContentType == "" {
up.ContentType = http.DetectContentType(data)
}
if location, etag, err = s.bfs.Upload(ctx, b.KeyID, b.KeySecret, up.ContentType, up.Bucket, up.Dir, up.FileName, data); err != nil {
return
}
uri, err := url.Parse(location)
if err != nil {
return
}
//todo: add user report
// http://info.bilibili.co/pages/viewpage.action?pageId=8474335
uInfo := &report.UserInfo{
Mid: 0,
Platform: "bfs-upload-interface",
Build: 1,
Business: 61, //bfs-upload
Action: action.String(), //操作类型
Ctime: time.Now(),
Index: []interface{}{location, up.Bucket, up.Dir, uri.Path}, // path is filename in bfs
Content: map[string]interface{}{
"upload_param": up,
},
}
report.User(uInfo)
result = &model.Result{
Location: location,
Etag: etag,
}
return
}
func (s *Service) dirlimit(bucket, dir, contentType string, data []byte) (err error) {
var (
width int
height int
dirlimit *model.DirConfig
ok bool
)
dir = strings.Trim(dir, "/")
if dirlimit, ok = s.bucketCache[bucket].DirLimit[dir]; !ok {
return
}
if len(dirlimit.Pic.AllowTypeSlice) != 0 {
var isAllow bool
for _, ctype := range dirlimit.Pic.AllowTypeSlice {
if ctype == contentType {
isAllow = true
}
}
if !isAllow {
log.Error("image content type illegal,bucket(%s),dir(%s),content type(%s)", bucket, dir, contentType)
err = ecode.BfsUploadFileContentTypeIllegal
return
}
}
if dirlimit.Pic.FileSize > 0 && len(data) > dirlimit.Pic.FileSize {
log.Error("data is too large, bucket(%s), dir(%s)", bucket, dir)
err = ecode.FileTooLarge
return
}
dataReader := bytes.NewReader(data)
if width, height, err = Pixel(dataReader); err != nil {
log.Error("get pixal error(%v), content-type maybe not support, bucket(%s), dir(%s)", err, bucket, dir)
err = nil
return
}
if (dirlimit.Pic.MinPixelWidthSize != 0 && width < dirlimit.Pic.MinPixelWidthSize) || (dirlimit.Pic.MaxPixelWidthSize != 0 && width > dirlimit.Pic.MaxPixelWidthSize) {
log.Error("image width illegal, bucket(%s), dir(%s)", bucket, dir)
err = ecode.BfsUploadFilePixelWidthIllegal
return
}
if (dirlimit.Pic.MinPixelWidthSize != 0 && height < dirlimit.Pic.MinPixelHeightSize) || (dirlimit.Pic.MaxPixelWidthSize != 0 && height > dirlimit.Pic.MaxPixelHeightSize) {
log.Error("image height illegal, bucket(%s), dir(%s)", bucket, dir)
err = ecode.BfsUploadFilePixelHeightIllegal
return
}
if dirlimit.Pic.MinAspectRatio != 0 && float64(width/height) < dirlimit.Pic.MinAspectRatio {
log.Error("image MinAspectRatio illegal, bucket(%s), dir(%s)", bucket, dir)
err = ecode.BfsUploadFilePixelAspectRatioIllegal
return
}
if dirlimit.Pic.MaxAspectRatio != 0 && float64(width/height) > dirlimit.Pic.MaxAspectRatio {
log.Error("image MaxAspectRatio illegal, bucket(%s), dir(%s)", bucket, dir)
err = ecode.BfsUploadFilePixelAspectRatioIllegal
return
}
return
}

View File

@@ -0,0 +1,71 @@
package service
import (
"context"
"crypto/sha1"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"testing"
"time"
"go-common/app/interface/main/upload/conf"
"go-common/app/interface/main/upload/model"
. "github.com/smartystreets/goconvey/convey"
)
func loadbs() []byte {
client := &http.Client{}
resp, err := client.Get("https://i0.hdslb.com/bfs/album/b11defd6410e9fa5b6962c3c5f0402be2608db8c.jpg")
if err != nil {
panic(err)
}
defer resp.Body.Close()
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
return bs
}
func TestGenImageUpload(t *testing.T) {
Convey("create image and upload it", t, func() {
res, err := svr.GenImageUpload(context.TODO(), "b4cfeeadca80f6f5", "c605dd5324f91ea1", "hello world", 2, true)
So(err, ShouldBeNil)
t.Logf("result:%+v", res)
})
}
func TestUpload(t *testing.T) {
bs := loadbs()
Convey("test Upload", t, func() {
now := time.Now().Unix()
sha1 := sha1.New()
sha1.Write([]byte(fmt.Sprintf("i love bilibili %s:%d", conf.Conf.Auths[0].AppSercet, now)))
token := fmt.Sprintf("%s:%d", hex.EncodeToString(sha1.Sum([]byte(""))), now)
result, err := svr.Upload(context.Background(), conf.Conf.Auths[0].AppKey, token, "", bs)
if err != nil {
t.Fatal(err.Error())
}
So("54aeb138b7fea2fe812aa8548f96cf1c0e4596ff", ShouldResemble, result.Etag)
})
}
func TestUploadRecord(t *testing.T) {
bs := loadbs()
Convey("test UploadRecord", t, func() {
ap := &model.UploadParam{
ContentType: "image/jpeg",
Bucket: "static",
FileName: "",
Dir: "",
}
result, err := svr.UploadRecord(context.Background(), model.UploadInternal, 11, ap, bs)
if err != nil {
t.Fatal(err.Error())
}
So("54aeb138b7fea2fe812aa8548f96cf1c0e4596ff", ShouldResemble, result.Etag)
})
}

View File

@@ -0,0 +1,70 @@
package service
import (
"context"
"time"
"go-common/app/interface/main/upload/conf"
"go-common/app/interface/main/upload/dao"
"go-common/app/interface/main/upload/model"
"go-common/library/log"
)
// Service .
type Service struct {
dao *dao.Dao
bfs *dao.Bfs
c *conf.Config
bucketCache map[string]*model.Bucket
}
// New .
func New(c *conf.Config) *Service {
s := &Service{
dao: dao.NewDao(c),
bfs: dao.NewBfs(c),
c: c,
bucketCache: make(map[string]*model.Bucket),
}
go s.cacheproc()
return s
}
// Ping .
func (s *Service) Ping(c context.Context) (err error) {
return
}
func (s *Service) cacheproc() {
for {
s.loadBucketCache()
time.Sleep(5 * time.Minute)
}
}
func (s *Service) loadBucketCache() {
var (
bMap map[string]*model.Bucket
err error
)
if bMap, err = s.dao.Buckets(); err != nil {
log.Error("get bucket meta failed! error(%v)", err)
return
}
s.bucketCache = bMap
}
// GetRateLimit return rate limit of bucket and dir
func (s *Service) GetRateLimit(bucket, dir string) (model.DirRateConfig, bool) {
b, ok := s.bucketCache[bucket]
if !ok {
return model.DirRateConfig{}, false
}
config, ok := b.DirLimit[dir]
if config == nil {
return model.DirRateConfig{}, false
}
return config.Rate, ok
}

View File

@@ -0,0 +1,29 @@
package service
import (
"flag"
"os"
"path/filepath"
"testing"
"go-common/app/interface/main/upload/conf"
)
var (
svr *Service
)
func TestMain(m *testing.M) {
var (
err error
)
dir, _ := filepath.Abs("../cmd/upload-test.toml")
if err = flag.Set("conf", dir); err != nil {
panic(err)
}
if err = conf.Init(); err != nil {
panic(err)
}
svr = New(conf.Conf)
os.Exit(m.Run())
}