mirror of https://github.com/cloudreve/Cloudreve
Feat: send remote uploading callback
parent
5b9de0e097
commit
64342fa88d
|
@ -61,7 +61,6 @@ func AuthRequired() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebDAVAuth 验证WebDAV登录及权限
|
// WebDAVAuth 验证WebDAV登录及权限
|
||||||
// TODO 测试
|
|
||||||
func WebDAVAuth() gin.HandlerFunc {
|
func WebDAVAuth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// OPTIONS 请求不需要鉴权,否则Windows10下无法保存文档
|
// OPTIONS 请求不需要鉴权,否则Windows10下无法保存文档
|
||||||
|
|
|
@ -103,7 +103,7 @@ func Init() {
|
||||||
if conf.SystemConfig.Mode == "master" {
|
if conf.SystemConfig.Mode == "master" {
|
||||||
secretKey = model.GetSettingByName("secret_key")
|
secretKey = model.GetSettingByName("secret_key")
|
||||||
} else {
|
} else {
|
||||||
secretKey = conf.SystemConfig.SlaveSecret
|
secretKey = conf.SlaveConfig.Secret
|
||||||
if secretKey == "" {
|
if secretKey == "" {
|
||||||
util.Log().Panic("未指定 SlaveSecret,请前往配置文件中指定")
|
util.Log().Panic("未指定 SlaveSecret,请前往配置文件中指定")
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,13 @@ type system struct {
|
||||||
Listen string `validate:"required"`
|
Listen string `validate:"required"`
|
||||||
Debug bool
|
Debug bool
|
||||||
SessionSecret string
|
SessionSecret string
|
||||||
SlaveSecret string `validate:"omitempty,gte=64"`
|
}
|
||||||
|
|
||||||
|
// slave 作为slave存储端配置
|
||||||
|
type slave struct {
|
||||||
|
Secret string `validate:"omitempty,gte=64"`
|
||||||
|
CallbackTimeout int `validate:"omitempty,gte=1"`
|
||||||
|
SignatureTTL int `validate:"omitempty,gte=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// captcha 验证码配置
|
// captcha 验证码配置
|
||||||
|
@ -82,6 +88,7 @@ func Init(path string) {
|
||||||
"Redis": RedisConfig,
|
"Redis": RedisConfig,
|
||||||
"Thumbnail": ThumbConfig,
|
"Thumbnail": ThumbConfig,
|
||||||
"CORS": CORSConfig,
|
"CORS": CORSConfig,
|
||||||
|
"Slave": SlaveConfig,
|
||||||
}
|
}
|
||||||
for sectionName, sectionStruct := range sections {
|
for sectionName, sectionStruct := range sections {
|
||||||
err = mapSection(sectionName, sectionStruct)
|
err = mapSection(sectionName, sectionStruct)
|
||||||
|
|
|
@ -40,13 +40,20 @@ var CaptchaConfig = &captcha{
|
||||||
var CORSConfig = &cors{
|
var CORSConfig = &cors{
|
||||||
AllowOrigins: []string{"UNSET"},
|
AllowOrigins: []string{"UNSET"},
|
||||||
AllowMethods: []string{"PUT", "POST", "GET", "OPTIONS"},
|
AllowMethods: []string{"PUT", "POST", "GET", "OPTIONS"},
|
||||||
AllowHeaders: []string{"Cookie", "Content-Length", "Content-Type", "X-Path", "X-FileName"},
|
AllowHeaders: []string{"Cookie", "X-Policy", "Authorization", "Content-Length", "Content-Type", "X-Path", "X-FileName"},
|
||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
ExposeHeaders: nil,
|
ExposeHeaders: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ThumbConfig 缩略图配置
|
||||||
var ThumbConfig = &thumb{
|
var ThumbConfig = &thumb{
|
||||||
MaxWidth: 400,
|
MaxWidth: 400,
|
||||||
MaxHeight: 300,
|
MaxHeight: 300,
|
||||||
FileSuffix: "._thumb",
|
FileSuffix: "._thumb",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SlaveConfig 从机配置
|
||||||
|
var SlaveConfig = &slave{
|
||||||
|
CallbackTimeout: 20,
|
||||||
|
SignatureTTL: 60,
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
model "github.com/HFO4/cloudreve/models"
|
model "github.com/HFO4/cloudreve/models"
|
||||||
"github.com/HFO4/cloudreve/pkg/conf"
|
"github.com/HFO4/cloudreve/pkg/conf"
|
||||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/request"
|
||||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||||
"github.com/HFO4/cloudreve/pkg/util"
|
"github.com/HFO4/cloudreve/pkg/util"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -212,6 +213,8 @@ func GenericAfterUpdate(ctx context.Context, fs *FileSystem) error {
|
||||||
// TODO 测试
|
// TODO 测试
|
||||||
func SlaveAfterUpload(ctx context.Context, fs *FileSystem) error {
|
func SlaveAfterUpload(ctx context.Context, fs *FileSystem) error {
|
||||||
fileHeader := ctx.Value(fsctx.FileHeaderCtx).(FileHeader)
|
fileHeader := ctx.Value(fsctx.FileHeaderCtx).(FileHeader)
|
||||||
|
policy := ctx.Value(fsctx.UploadPolicyCtx).(serializer.UploadPolicy)
|
||||||
|
|
||||||
// 构造一个model.File,用于生成缩略图
|
// 构造一个model.File,用于生成缩略图
|
||||||
file := model.File{
|
file := model.File{
|
||||||
Name: fileHeader.GetFileName(),
|
Name: fileHeader.GetFileName(),
|
||||||
|
@ -219,9 +222,13 @@ func SlaveAfterUpload(ctx context.Context, fs *FileSystem) error {
|
||||||
}
|
}
|
||||||
fs.GenerateThumbnail(ctx, &file)
|
fs.GenerateThumbnail(ctx, &file)
|
||||||
|
|
||||||
// TODO 发送回调请求
|
// 发送回调请求
|
||||||
|
callbackBody := serializer.UploadCallback{
|
||||||
return nil
|
Name: file.Name,
|
||||||
|
SourceName: file.SourceName,
|
||||||
|
PicInfo: file.PicInfo,
|
||||||
|
}
|
||||||
|
return request.RemoteCallback(policy.CallbackURL, callbackBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenericAfterUpload 文件上传完成后,包含数据库操作
|
// GenericAfterUpload 文件上传完成后,包含数据库操作
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/auth"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/conf"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/util"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoteCallback 发送远程存储策略上传回调请求
|
||||||
|
func RemoteCallback(url string, body serializer.UploadCallback) error {
|
||||||
|
callbackBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return serializer.NewError(serializer.CodeCallbackError, "无法编码回调正文", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := generalClient.Request(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
bytes.NewReader(callbackBody),
|
||||||
|
WithTimeout(time.Duration(conf.SlaveConfig.CallbackTimeout)*time.Second),
|
||||||
|
WithCredential(auth.General, int64(conf.SlaveConfig.SignatureTTL)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.Err != nil {
|
||||||
|
return serializer.NewError(serializer.CodeCallbackError, "无法发起回调请求", resp.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查返回HTTP状态码
|
||||||
|
if resp.Response.StatusCode != 200 {
|
||||||
|
util.Log().Debug("服务端返回非正常状态码:%d", resp.Response.StatusCode)
|
||||||
|
return serializer.NewError(serializer.CodeCallbackError, "服务端返回非正常状态码", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查返回API状态码
|
||||||
|
var response serializer.Response
|
||||||
|
rawResp, err := ioutil.ReadAll(resp.Response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return serializer.NewError(serializer.CodeCallbackError, "无法读取响应正文", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析回调服务端响应
|
||||||
|
err = json.Unmarshal(rawResp, &response)
|
||||||
|
if err != nil {
|
||||||
|
util.Log().Debug("无法解析回调服务端响应:%s", string(rawResp))
|
||||||
|
return serializer.NewError(serializer.CodeCallbackError, "无法解析服务端返回的响应", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Code != 0 {
|
||||||
|
return serializer.NewError(response.Code, response.Msg, errors.New(response.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
testMock "github.com/stretchr/testify/mock"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoteCallback(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
|
||||||
|
// 回调成功
|
||||||
|
{
|
||||||
|
clientMock := ClientMock{}
|
||||||
|
mockResp, _ := json.Marshal(serializer.Response{Code: 0})
|
||||||
|
clientMock.On(
|
||||||
|
"Request",
|
||||||
|
"POST",
|
||||||
|
"http://test/test/url",
|
||||||
|
testMock.Anything,
|
||||||
|
testMock.Anything,
|
||||||
|
).Return(Response{
|
||||||
|
Err: nil,
|
||||||
|
Response: &http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader(mockResp)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
generalClient = clientMock
|
||||||
|
resp := RemoteCallback("http://test/test/url", serializer.UploadCallback{
|
||||||
|
SourceName: "source",
|
||||||
|
})
|
||||||
|
asserts.NoError(resp)
|
||||||
|
clientMock.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务端返回业务错误
|
||||||
|
{
|
||||||
|
clientMock := ClientMock{}
|
||||||
|
mockResp, _ := json.Marshal(serializer.Response{Code: 401})
|
||||||
|
clientMock.On(
|
||||||
|
"Request",
|
||||||
|
"POST",
|
||||||
|
"http://test/test/url",
|
||||||
|
testMock.Anything,
|
||||||
|
testMock.Anything,
|
||||||
|
).Return(Response{
|
||||||
|
Err: nil,
|
||||||
|
Response: &http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader(mockResp)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
generalClient = clientMock
|
||||||
|
resp := RemoteCallback("http://test/test/url", serializer.UploadCallback{
|
||||||
|
SourceName: "source",
|
||||||
|
})
|
||||||
|
asserts.EqualValues(401, resp.(serializer.AppError).Code)
|
||||||
|
clientMock.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无法解析回调响应
|
||||||
|
{
|
||||||
|
clientMock := ClientMock{}
|
||||||
|
clientMock.On(
|
||||||
|
"Request",
|
||||||
|
"POST",
|
||||||
|
"http://test/test/url",
|
||||||
|
testMock.Anything,
|
||||||
|
testMock.Anything,
|
||||||
|
).Return(Response{
|
||||||
|
Err: nil,
|
||||||
|
Response: &http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: ioutil.NopCloser(strings.NewReader("mockResp")),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
generalClient = clientMock
|
||||||
|
resp := RemoteCallback("http://test/test/url", serializer.UploadCallback{
|
||||||
|
SourceName: "source",
|
||||||
|
})
|
||||||
|
asserts.Error(resp)
|
||||||
|
clientMock.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP状态码非200
|
||||||
|
{
|
||||||
|
clientMock := ClientMock{}
|
||||||
|
clientMock.On(
|
||||||
|
"Request",
|
||||||
|
"POST",
|
||||||
|
"http://test/test/url",
|
||||||
|
testMock.Anything,
|
||||||
|
testMock.Anything,
|
||||||
|
).Return(Response{
|
||||||
|
Err: nil,
|
||||||
|
Response: &http.Response{
|
||||||
|
StatusCode: 404,
|
||||||
|
Body: ioutil.NopCloser(strings.NewReader("mockResp")),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
generalClient = clientMock
|
||||||
|
resp := RemoteCallback("http://test/test/url", serializer.UploadCallback{
|
||||||
|
SourceName: "source",
|
||||||
|
})
|
||||||
|
asserts.Error(resp)
|
||||||
|
clientMock.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无法发起回调
|
||||||
|
{
|
||||||
|
clientMock := ClientMock{}
|
||||||
|
clientMock.On(
|
||||||
|
"Request",
|
||||||
|
"POST",
|
||||||
|
"http://test/test/url",
|
||||||
|
testMock.Anything,
|
||||||
|
testMock.Anything,
|
||||||
|
).Return(Response{
|
||||||
|
Err: errors.New("error"),
|
||||||
|
})
|
||||||
|
generalClient = clientMock
|
||||||
|
resp := RemoteCallback("http://test/test/url", serializer.UploadCallback{
|
||||||
|
SourceName: "source",
|
||||||
|
})
|
||||||
|
asserts.Error(resp)
|
||||||
|
clientMock.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/HFO4/cloudreve/pkg/auth"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var generalClient Client = HTTPClient{}
|
||||||
|
|
||||||
|
// Response 请求的响应或错误信息
|
||||||
|
type Response struct {
|
||||||
|
Err error
|
||||||
|
Response *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client 请求客户端
|
||||||
|
type Client interface {
|
||||||
|
Request(method, target string, body io.Reader, opts ...Option) Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPClient 实现 Client 接口
|
||||||
|
type HTTPClient struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 发送请求的额外设置
|
||||||
|
type Option interface {
|
||||||
|
apply(*options)
|
||||||
|
}
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
timeout time.Duration
|
||||||
|
header http.Header
|
||||||
|
sign auth.Auth
|
||||||
|
signTTL int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type optionFunc func(*options)
|
||||||
|
|
||||||
|
func (f optionFunc) apply(o *options) {
|
||||||
|
f(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultOption() *options {
|
||||||
|
return &options{
|
||||||
|
header: http.Header{},
|
||||||
|
timeout: time.Duration(30) * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout 设置请求超时
|
||||||
|
func WithTimeout(t time.Duration) Option {
|
||||||
|
return optionFunc(func(o *options) {
|
||||||
|
o.timeout = t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCredential 对请求进行签名
|
||||||
|
func WithCredential(instance auth.Auth, ttl int64) Option {
|
||||||
|
return optionFunc(func(o *options) {
|
||||||
|
o.sign = instance
|
||||||
|
o.signTTL = ttl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeader 设置请求Header
|
||||||
|
func WithHeader(header http.Header) Option {
|
||||||
|
return optionFunc(func(o *options) {
|
||||||
|
o.header = header
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request 发送HTTP请求
|
||||||
|
func (c HTTPClient) Request(method, target string, body io.Reader, opts ...Option) Response {
|
||||||
|
// 应用额外设置
|
||||||
|
options := newDefaultOption()
|
||||||
|
for _, o := range opts {
|
||||||
|
o.apply(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建请求客户端
|
||||||
|
client := &http.Client{Timeout: options.timeout}
|
||||||
|
|
||||||
|
// 创建请求
|
||||||
|
req, err := http.NewRequest(method, target, body)
|
||||||
|
if err != nil {
|
||||||
|
return Response{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加请求header
|
||||||
|
req.Header = options.header
|
||||||
|
|
||||||
|
// 签名请求
|
||||||
|
if options.sign != nil {
|
||||||
|
auth.SignRequest(options.sign, req, time.Now().Unix()+options.signTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return Response{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response{Err: nil, Response: resp}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/HFO4/cloudreve/pkg/auth"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
testMock "github.com/stretchr/testify/mock"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientMock struct {
|
||||||
|
testMock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ClientMock) Request(method, target string, body io.Reader, opts ...Option) Response {
|
||||||
|
args := m.Called(method, target, body, opts)
|
||||||
|
return args.Get(0).(Response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTimeout(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
options := newDefaultOption()
|
||||||
|
WithTimeout(time.Duration(5) * time.Second).apply(options)
|
||||||
|
asserts.Equal(time.Duration(5)*time.Second, options.timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithHeader(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
options := newDefaultOption()
|
||||||
|
WithHeader(map[string][]string{"Origin": []string{"123"}}).apply(options)
|
||||||
|
asserts.Equal(http.Header{"Origin": []string{"123"}}, options.header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCredential(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
options := newDefaultOption()
|
||||||
|
WithCredential(auth.HMACAuth{SecretKey: []byte("123")}, 10).apply(options)
|
||||||
|
asserts.Equal(auth.HMACAuth{SecretKey: []byte("123")}, options.sign)
|
||||||
|
asserts.EqualValues(10, options.signTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPClient_Request(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
client := HTTPClient{}
|
||||||
|
|
||||||
|
// 正常
|
||||||
|
{
|
||||||
|
resp := client.Request(
|
||||||
|
"GET",
|
||||||
|
"http://cloudreveisnotexist.com",
|
||||||
|
strings.NewReader(""),
|
||||||
|
WithTimeout(time.Duration(1)*time.Microsecond),
|
||||||
|
WithCredential(auth.HMACAuth{SecretKey: []byte("123")}, 10),
|
||||||
|
)
|
||||||
|
asserts.Error(resp.Err)
|
||||||
|
asserts.Nil(resp.Response)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -70,6 +70,8 @@ const (
|
||||||
CodeInternalSetting = 50005
|
CodeInternalSetting = 50005
|
||||||
// CodeCacheOperation 缓存操作失败
|
// CodeCacheOperation 缓存操作失败
|
||||||
CodeCacheOperation = 50006
|
CodeCacheOperation = 50006
|
||||||
|
// CodeCallbackError 回调失败
|
||||||
|
CodeCallbackError = 50007
|
||||||
//CodeParamErr 各种奇奇怪怪的参数错误
|
//CodeParamErr 各种奇奇怪怪的参数错误
|
||||||
CodeParamErr = 40001
|
CodeParamErr = 40001
|
||||||
// CodeNotSet 未定错误,后续尝试从error中获取
|
// CodeNotSet 未定错误,后续尝试从error中获取
|
||||||
|
@ -94,13 +96,13 @@ func ParamErr(msg string, err error) Response {
|
||||||
|
|
||||||
// Err 通用错误处理
|
// Err 通用错误处理
|
||||||
func Err(errCode int, msg string, err error) Response {
|
func Err(errCode int, msg string, err error) Response {
|
||||||
// 如果错误code未定,则尝试从AppError中获取
|
// 底层错误是AppError,则尝试从AppError中获取详细信息
|
||||||
if errCode == CodeNotSet {
|
if appError, ok := err.(AppError); ok {
|
||||||
if appError, ok := err.(AppError); ok {
|
errCode = appError.Code
|
||||||
errCode = appError.Code
|
err = appError.RawError
|
||||||
err = appError.RawError
|
msg = appError.Msg
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res := Response{
|
res := Response{
|
||||||
Code: errCode,
|
Code: errCode,
|
||||||
Msg: msg,
|
Msg: msg,
|
||||||
|
|
|
@ -28,6 +28,13 @@ type UploadSession struct {
|
||||||
VirtualPath string
|
VirtualPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadCallback 远程存储策略上传回调正文
|
||||||
|
type UploadCallback struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
SourceName string `json:"source_name"`
|
||||||
|
PicInfo string `json:"pic_info"`
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gob.Register(UploadSession{})
|
gob.Register(UploadSession{})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue