mirror of
https://github.com/halejohn/Cloudreve.git
synced 2026-01-26 09:34:57 +08:00
Feat: upyun download / thumb / sign
This commit is contained in:
38
pkg/filesystem/driver/local/file.go
Normal file
38
pkg/filesystem/driver/local/file.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// FileStream 用户传来的文件
|
||||
type FileStream struct {
|
||||
File io.ReadCloser
|
||||
Size uint64
|
||||
VirtualPath string
|
||||
Name string
|
||||
MIMEType string
|
||||
}
|
||||
|
||||
func (file FileStream) Read(p []byte) (n int, err error) {
|
||||
return file.File.Read(p)
|
||||
}
|
||||
|
||||
func (file FileStream) GetMIMEType() string {
|
||||
return file.MIMEType
|
||||
}
|
||||
|
||||
func (file FileStream) GetSize() uint64 {
|
||||
return file.Size
|
||||
}
|
||||
|
||||
func (file FileStream) Close() error {
|
||||
return file.File.Close()
|
||||
}
|
||||
|
||||
func (file FileStream) GetFileName() string {
|
||||
return file.Name
|
||||
}
|
||||
|
||||
func (file FileStream) GetVirtualPath() string {
|
||||
return file.VirtualPath
|
||||
}
|
||||
48
pkg/filesystem/driver/local/file_test.go
Normal file
48
pkg/filesystem/driver/local/file_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileStream_GetFileName(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
file := FileStream{Name: "123"}
|
||||
asserts.Equal("123", file.GetFileName())
|
||||
}
|
||||
|
||||
func TestFileStream_GetMIMEType(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
file := FileStream{MIMEType: "123"}
|
||||
asserts.Equal("123", file.GetMIMEType())
|
||||
}
|
||||
|
||||
func TestFileStream_GetSize(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
file := FileStream{Size: 123}
|
||||
asserts.Equal(uint64(123), file.GetSize())
|
||||
}
|
||||
|
||||
func TestFileStream_Read(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
file := FileStream{
|
||||
File: ioutil.NopCloser(strings.NewReader("123")),
|
||||
}
|
||||
var p = make([]byte, 3)
|
||||
{
|
||||
n, err := file.Read(p)
|
||||
asserts.Equal(3, n)
|
||||
asserts.NoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileStream_Close(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
file := FileStream{
|
||||
File: ioutil.NopCloser(strings.NewReader("123")),
|
||||
}
|
||||
err := file.Close()
|
||||
asserts.NoError(err)
|
||||
}
|
||||
165
pkg/filesystem/driver/local/handler.go
Normal file
165
pkg/filesystem/driver/local/handler.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/auth"
|
||||
"github.com/HFO4/cloudreve/pkg/cache"
|
||||
"github.com/HFO4/cloudreve/pkg/conf"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/response"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Driver 本地策略适配器
|
||||
type Driver struct {
|
||||
Policy *model.Policy
|
||||
}
|
||||
|
||||
// Get 获取文件内容
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
util.Log().Debug("无法打开文件:%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 开启一个协程,用于请求结束后关闭reader
|
||||
// go closeReader(ctx, file)
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// closeReader 用于在请求结束后关闭reader
|
||||
// TODO 让业务代码自己关闭
|
||||
func closeReader(ctx context.Context, closer io.Closer) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = closer.Close()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
defer file.Close()
|
||||
dst = filepath.FromSlash(dst)
|
||||
|
||||
// 如果目标目录不存在,创建
|
||||
basePath := filepath.Dir(dst)
|
||||
if !util.Exists(basePath) {
|
||||
err := os.MkdirAll(basePath, 0700)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法创建目录,%s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 创建目标文件
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
util.Log().Warning("无法创建文件,%s", err)
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 写入文件内容
|
||||
_, err = io.Copy(out, file)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除一个或多个文件,
|
||||
// 返回未删除的文件,及遇到的最后一个错误
|
||||
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
deleteFailed := make([]string, 0, len(files))
|
||||
var retErr error
|
||||
|
||||
for _, value := range files {
|
||||
err := os.Remove(filepath.FromSlash(value))
|
||||
if err != nil {
|
||||
util.Log().Warning("无法删除文件,%s", err)
|
||||
retErr = err
|
||||
deleteFailed = append(deleteFailed, value)
|
||||
}
|
||||
|
||||
// 尝试删除文件的缩略图(如果有)
|
||||
_ = os.Remove(value + conf.ThumbConfig.FileSuffix)
|
||||
}
|
||||
|
||||
return deleteFailed, retErr
|
||||
}
|
||||
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
file, err := handler.Get(ctx, path+conf.ThumbConfig.FileSuffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.ContentResponse{
|
||||
Redirect: false,
|
||||
Content: file,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Source 获取外链URL
|
||||
func (handler Driver) Source(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
baseURL url.URL,
|
||||
ttl int64,
|
||||
isDownload bool,
|
||||
speed int,
|
||||
) (string, error) {
|
||||
file, ok := ctx.Value(fsctx.FileModelCtx).(model.File)
|
||||
if !ok {
|
||||
return "", errors.New("无法获取文件记录上下文")
|
||||
}
|
||||
|
||||
var (
|
||||
signedURI *url.URL
|
||||
err error
|
||||
)
|
||||
if isDownload {
|
||||
// 创建下载会话,将文件信息写入缓存
|
||||
downloadSessionID := util.RandStringRunes(16)
|
||||
err = cache.Set("download_"+downloadSessionID, file, int(ttl))
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeCacheOperation, "无法创建下载会话", err)
|
||||
}
|
||||
|
||||
// 签名生成文件记录
|
||||
signedURI, err = auth.SignURI(
|
||||
auth.General,
|
||||
fmt.Sprintf("/api/v3/file/download/%s", downloadSessionID),
|
||||
ttl,
|
||||
)
|
||||
} else {
|
||||
// 签名生成文件记录
|
||||
signedURI, err = auth.SignURI(
|
||||
auth.General,
|
||||
fmt.Sprintf("/api/v3/file/get/%d/%s", file.ID, file.Name),
|
||||
ttl,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeEncryptError, "无法对URL进行签名", err)
|
||||
}
|
||||
|
||||
finalURL := baseURL.ResolveReference(signedURI).String()
|
||||
return finalURL, nil
|
||||
}
|
||||
|
||||
// Token 获取上传策略和认证Token,本地策略直接返回空值
|
||||
func (handler Driver) Token(ctx context.Context, ttl int64, key string) (serializer.UploadCredential, error) {
|
||||
return serializer.UploadCredential{}, nil
|
||||
}
|
||||
194
pkg/filesystem/driver/local/handller_test.go
Normal file
194
pkg/filesystem/driver/local/handller_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/auth"
|
||||
"github.com/HFO4/cloudreve/pkg/conf"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandler_Put(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
file io.ReadCloser
|
||||
dst string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
file: ioutil.NopCloser(strings.NewReader("test input file")),
|
||||
dst: "test/test/txt",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
file: ioutil.NopCloser(strings.NewReader("test input file")),
|
||||
dst: "/notexist:/S.TXT",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := handler.Put(ctx, testCase.file, testCase.dst, 15)
|
||||
if testCase.err {
|
||||
asserts.Error(err)
|
||||
} else {
|
||||
asserts.NoError(err)
|
||||
asserts.True(util.Exists(testCase.dst))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Delete(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx := context.Background()
|
||||
|
||||
file, err := os.Create("test.file")
|
||||
asserts.NoError(err)
|
||||
_ = file.Close()
|
||||
list, err := handler.Delete(ctx, []string{"test.file"})
|
||||
asserts.Equal([]string{}, list)
|
||||
asserts.NoError(err)
|
||||
|
||||
file, err = os.Create("test.file")
|
||||
asserts.NoError(err)
|
||||
_ = file.Close()
|
||||
list, err = handler.Delete(ctx, []string{"test.file", "test.notexist"})
|
||||
asserts.Equal([]string{"test.notexist"}, list)
|
||||
asserts.Error(err)
|
||||
|
||||
list, err = handler.Delete(ctx, []string{"test.notexist"})
|
||||
asserts.Equal([]string{"test.notexist"}, list)
|
||||
asserts.Error(err)
|
||||
}
|
||||
|
||||
func TestHandler_Get(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 成功
|
||||
file, err := os.Create("TestHandler_Get.txt")
|
||||
asserts.NoError(err)
|
||||
_ = file.Close()
|
||||
|
||||
rs, err := handler.Get(ctx, "TestHandler_Get.txt")
|
||||
asserts.NoError(err)
|
||||
asserts.NotNil(rs)
|
||||
|
||||
// 文件不存在
|
||||
|
||||
rs, err = handler.Get(ctx, "TestHandler_Get_notExist.txt")
|
||||
asserts.Error(err)
|
||||
asserts.Nil(rs)
|
||||
}
|
||||
|
||||
func TestHandler_Thumb(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx := context.Background()
|
||||
file, err := os.Create("TestHandler_Thumb" + conf.ThumbConfig.FileSuffix)
|
||||
asserts.NoError(err)
|
||||
file.Close()
|
||||
|
||||
// 正常
|
||||
{
|
||||
thumb, err := handler.Thumb(ctx, "TestHandler_Thumb")
|
||||
asserts.NoError(err)
|
||||
asserts.NotNil(thumb.Content)
|
||||
}
|
||||
|
||||
// 不存在
|
||||
{
|
||||
_, err := handler.Thumb(ctx, "not_exist")
|
||||
asserts.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Source(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx := context.Background()
|
||||
auth.General = auth.HMACAuth{SecretKey: []byte("test")}
|
||||
|
||||
// 成功
|
||||
{
|
||||
file := model.File{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
},
|
||||
Name: "test.jpg",
|
||||
}
|
||||
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
|
||||
baseURL, err := url.Parse("https://cloudreve.org")
|
||||
asserts.NoError(err)
|
||||
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, false, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.NotEmpty(sourceURL)
|
||||
asserts.Contains(sourceURL, "sign=")
|
||||
asserts.Contains(sourceURL, "https://cloudreve.org")
|
||||
}
|
||||
|
||||
// 无法获取上下文
|
||||
{
|
||||
baseURL, err := url.Parse("https://cloudreve.org")
|
||||
asserts.NoError(err)
|
||||
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, false, 0)
|
||||
asserts.Error(err)
|
||||
asserts.Empty(sourceURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetDownloadURL(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx := context.Background()
|
||||
auth.General = auth.HMACAuth{SecretKey: []byte("test")}
|
||||
|
||||
// 成功
|
||||
{
|
||||
file := model.File{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
},
|
||||
Name: "test.jpg",
|
||||
}
|
||||
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
|
||||
baseURL, err := url.Parse("https://cloudreve.org")
|
||||
asserts.NoError(err)
|
||||
downloadURL, err := handler.Source(ctx, "", *baseURL, 10, true, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.Contains(downloadURL, "sign=")
|
||||
asserts.Contains(downloadURL, "https://cloudreve.org")
|
||||
}
|
||||
|
||||
// 无法获取上下文
|
||||
{
|
||||
baseURL, err := url.Parse("https://cloudreve.org")
|
||||
asserts.NoError(err)
|
||||
downloadURL, err := handler.Source(ctx, "", *baseURL, 10, true, 0)
|
||||
asserts.Error(err)
|
||||
asserts.Empty(downloadURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Token(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{}
|
||||
ctx := context.Background()
|
||||
_, err := handler.Token(ctx, 10, "123")
|
||||
asserts.NoError(err)
|
||||
}
|
||||
116
pkg/filesystem/driver/oss/callback.go
Normal file
116
pkg/filesystem/driver/oss/callback.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package oss
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/md5"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/HFO4/cloudreve/pkg/cache"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetPublicKey 从回调请求或缓存中获取OSS的回调签名公钥
|
||||
func GetPublicKey(r *http.Request) ([]byte, error) {
|
||||
var pubKey []byte
|
||||
|
||||
// 尝试从缓存中获取
|
||||
pub, exist := cache.Get("oss_public_key")
|
||||
if exist {
|
||||
return pub.([]byte), nil
|
||||
}
|
||||
|
||||
// 从请求中获取
|
||||
pubURL, err := base64.StdEncoding.DecodeString(r.Header.Get("x-oss-pub-key-url"))
|
||||
if err != nil {
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
// 确保这个 public key 是由 OSS 颁发的
|
||||
if !strings.HasPrefix(string(pubURL), "http://gosspublic.alicdn.com/") &&
|
||||
!strings.HasPrefix(string(pubURL), "https://gosspublic.alicdn.com/") {
|
||||
return pubKey, errors.New("公钥URL无效")
|
||||
}
|
||||
|
||||
// 获取公钥
|
||||
client := request.HTTPClient{}
|
||||
body, err := client.Request("GET", string(pubURL), nil).
|
||||
CheckHTTPResponse(200).
|
||||
GetResponse()
|
||||
if err != nil {
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
_ = cache.Set("oss_public_key", []byte(body), 86400*7)
|
||||
|
||||
return []byte(body), nil
|
||||
}
|
||||
|
||||
func getRequestMD5(r *http.Request) ([]byte, error) {
|
||||
var byteMD5 []byte
|
||||
|
||||
// 获取请求正文
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
return byteMD5, err
|
||||
}
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
|
||||
strURLPathDecode, err := url.PathUnescape(r.URL.Path)
|
||||
if err != nil {
|
||||
return byteMD5, err
|
||||
}
|
||||
|
||||
strAuth := fmt.Sprintf("%s\n%s", strURLPathDecode, string(body))
|
||||
md5Ctx := md5.New()
|
||||
md5Ctx.Write([]byte(strAuth))
|
||||
byteMD5 = md5Ctx.Sum(nil)
|
||||
|
||||
return byteMD5, nil
|
||||
}
|
||||
|
||||
// VerifyCallbackSignature 验证OSS回调请求
|
||||
func VerifyCallbackSignature(r *http.Request) error {
|
||||
bytePublicKey, err := GetPublicKey(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
byteMD5, err := getRequestMD5(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
strAuthorizationBase64 := r.Header.Get("authorization")
|
||||
if strAuthorizationBase64 == "" {
|
||||
return errors.New("no authorization field in Request header")
|
||||
}
|
||||
authorization, _ := base64.StdEncoding.DecodeString(strAuthorizationBase64)
|
||||
|
||||
pubBlock, _ := pem.Decode(bytePublicKey)
|
||||
if pubBlock == nil {
|
||||
return errors.New("pubBlock not exist")
|
||||
}
|
||||
pubInterface, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
|
||||
if (pubInterface == nil) || (err != nil) {
|
||||
return err
|
||||
}
|
||||
pub := pubInterface.(*rsa.PublicKey)
|
||||
|
||||
errorVerifyPKCS1v15 := rsa.VerifyPKCS1v15(pub, crypto.MD5, byteMD5, authorization)
|
||||
if errorVerifyPKCS1v15 != nil {
|
||||
return errorVerifyPKCS1v15
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
192
pkg/filesystem/driver/oss/callback_test.go
Normal file
192
pkg/filesystem/driver/oss/callback_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package oss
|
||||
|
||||
import (
|
||||
"github.com/HFO4/cloudreve/pkg/cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetPublicKey(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
testCases := []struct {
|
||||
Request http.Request
|
||||
ResNil bool
|
||||
Error bool
|
||||
}{
|
||||
// Header解码失败
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: http.Header{
|
||||
"X-Oss-Pub-Key-Url": {"中文"},
|
||||
},
|
||||
},
|
||||
ResNil: true,
|
||||
Error: true,
|
||||
},
|
||||
// 公钥URL无效
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: http.Header{
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9wb3JuaHViLmNvbQ=="},
|
||||
},
|
||||
},
|
||||
ResNil: true,
|
||||
Error: true,
|
||||
},
|
||||
// 请求失败
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: http.Header{
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cDovL2dvc3NwdWJsaWMuYWxpY2RuLmNvbS8yMzQyMzQ="},
|
||||
},
|
||||
},
|
||||
ResNil: true,
|
||||
Error: true,
|
||||
},
|
||||
// 成功
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: http.Header{
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cDovL2dvc3NwdWJsaWMuYWxpY2RuLmNvbS9jYWxsYmFja19wdWJfa2V5X3YxLnBlbQ=="},
|
||||
},
|
||||
},
|
||||
ResNil: false,
|
||||
Error: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
asserts.NoError(cache.Deletes([]string{"oss_public_key"}, ""))
|
||||
res, err := GetPublicKey(&testCase.Request)
|
||||
if testCase.Error {
|
||||
asserts.Error(err, "Test Case #%d", i)
|
||||
} else {
|
||||
asserts.NoError(err, "Test Case #%d", i)
|
||||
}
|
||||
if testCase.ResNil {
|
||||
asserts.Empty(res, "Test Case #%d", i)
|
||||
} else {
|
||||
asserts.NotEmpty(res, "Test Case #%d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试缓存
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte("123"), 0))
|
||||
res, err := GetPublicKey(nil)
|
||||
asserts.NoError(err)
|
||||
asserts.Equal([]byte("123"), res)
|
||||
}
|
||||
|
||||
func TestVerifyCallbackSignature(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
testPubKey := `-----BEGIN PUBLIC KEY-----
|
||||
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKs/JBGzwUB2aVht4crBx3oIPBLNsjGs
|
||||
C0fTXv+nvlmklvkcolvpvXLTjaxUHR3W9LXxQ2EHXAJfCB+6H2YF1k8CAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
`
|
||||
|
||||
// 成功
|
||||
{
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte(testPubKey), 0))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "/api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH"},
|
||||
Header: map[string][]string{
|
||||
"Authorization": {"e5LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw=="},
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.NoError(VerifyCallbackSignature(&r))
|
||||
}
|
||||
|
||||
// 签名错误
|
||||
{
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte(testPubKey), 0))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "/api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH"},
|
||||
Header: map[string][]string{
|
||||
"Authorization": {"e3LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw=="},
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.Error(VerifyCallbackSignature(&r))
|
||||
}
|
||||
|
||||
// GetPubKey 失败
|
||||
{
|
||||
asserts.NoError(cache.Deletes([]string{"oss_public_key"}, ""))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "/api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH"},
|
||||
Header: map[string][]string{
|
||||
"Authorization": {"e5LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw=="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.Error(VerifyCallbackSignature(&r))
|
||||
}
|
||||
|
||||
// getRequestMD5 失败
|
||||
{
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte(testPubKey), 0))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "%测试"},
|
||||
Header: map[string][]string{
|
||||
"Authorization": {"e5LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw=="},
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.Error(VerifyCallbackSignature(&r))
|
||||
}
|
||||
|
||||
// 无 Authorization 头
|
||||
{
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte(testPubKey), 0))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "/api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH"},
|
||||
Header: map[string][]string{
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.Error(VerifyCallbackSignature(&r))
|
||||
}
|
||||
|
||||
// pub block 不存在
|
||||
{
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte(""), 0))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "/api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH"},
|
||||
Header: map[string][]string{
|
||||
"Authorization": {"e5LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw=="},
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.Error(VerifyCallbackSignature(&r))
|
||||
}
|
||||
|
||||
// ParsePKIXPublicKey出错
|
||||
{
|
||||
asserts.NoError(cache.Set("oss_public_key", []byte("-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----"), 0))
|
||||
r := http.Request{
|
||||
URL: &url.URL{Path: "/api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH"},
|
||||
Header: map[string][]string{
|
||||
"Authorization": {"e5LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw=="},
|
||||
"X-Oss-Pub-Key-Url": {"aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0="},
|
||||
},
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}`)),
|
||||
}
|
||||
asserts.Error(VerifyCallbackSignature(&r))
|
||||
}
|
||||
}
|
||||
|
||||
///api/v3/callback/oss/TnXx5E5VyfJUyM1UdkdDu1rtnJ34EbmH
|
||||
//{"name":"2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","source_name":"1/1_hFRtDLgM_2f7b2ccf30e9270ea920f1ab8a4037a546a2f0d5.jpg","size":114020,"pic_info":"810,539"}
|
||||
// aHR0cHM6Ly9nb3NzcHVibGljLmFsaWNkbi5jb20vY2FsbGJhY2tfcHViX2tleV92MS5wZW0=
|
||||
// e5LwzwTkP9AFAItT4YzvdJOHd0Y0wqTMWhsV/h5SG90JYGAmMd+8LQyj96R+9qUfJWjMt6suuUh7LaOryR87Dw==
|
||||
270
pkg/filesystem/driver/oss/handler_test.go
Normal file
270
pkg/filesystem/driver/oss/handler_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package oss
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/cache"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"github.com/stretchr/testify/assert"
|
||||
testMock "github.com/stretchr/testify/mock"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDriver_InitOSSClient(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "test.com",
|
||||
},
|
||||
}
|
||||
|
||||
// 成功
|
||||
{
|
||||
asserts.NoError(handler.InitOSSClient())
|
||||
}
|
||||
|
||||
// 未指定存储策略
|
||||
{
|
||||
handler := Driver{}
|
||||
asserts.Error(handler.InitOSSClient())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_Token(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "test.com",
|
||||
},
|
||||
}
|
||||
|
||||
// 成功
|
||||
{
|
||||
ctx := context.WithValue(context.Background(), fsctx.SavePathCtx, "/123")
|
||||
cache.Set("setting_siteURL", "http://test.cloudreve.org", 0)
|
||||
res, err := handler.Token(ctx, 10, "key")
|
||||
asserts.NoError(err)
|
||||
asserts.NotEmpty(res.Policy)
|
||||
asserts.NotEmpty(res.Token)
|
||||
asserts.Equal(handler.Policy.AccessKey, res.AccessKey)
|
||||
asserts.Equal("/123", res.Path)
|
||||
}
|
||||
|
||||
// 上下文错误
|
||||
{
|
||||
ctx := context.Background()
|
||||
_, err := handler.Token(ctx, 10, "key")
|
||||
asserts.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDriver_Source(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "test.com",
|
||||
},
|
||||
}
|
||||
|
||||
// 正常 非下载 无限速
|
||||
{
|
||||
res, err := handler.Source(context.Background(), "/123", url.URL{}, 10, false, 0)
|
||||
asserts.NoError(err)
|
||||
resURL, err := url.Parse(res)
|
||||
asserts.NoError(err)
|
||||
query := resURL.Query()
|
||||
asserts.NotEmpty(query.Get("Signature"))
|
||||
asserts.NotEmpty(query.Get("Expires"))
|
||||
asserts.Equal("ak", query.Get("OSSAccessKeyId"))
|
||||
}
|
||||
|
||||
// 限速 + 下载
|
||||
{
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, model.File{Name: "123.txt"})
|
||||
res, err := handler.Source(ctx, "/123", url.URL{}, 10, true, 819201)
|
||||
asserts.NoError(err)
|
||||
resURL, err := url.Parse(res)
|
||||
asserts.NoError(err)
|
||||
query := resURL.Query()
|
||||
asserts.NotEmpty(query.Get("Signature"))
|
||||
asserts.NotEmpty(query.Get("Expires"))
|
||||
asserts.Equal("ak", query.Get("OSSAccessKeyId"))
|
||||
asserts.EqualValues("819201", query.Get("x-oss-traffic-limit"))
|
||||
asserts.NotEmpty(query.Get("response-content-disposition"))
|
||||
}
|
||||
|
||||
// 限速超出范围 + 下载
|
||||
{
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, model.File{Name: "123.txt"})
|
||||
res, err := handler.Source(ctx, "/123", url.URL{}, 10, true, 10)
|
||||
asserts.NoError(err)
|
||||
resURL, err := url.Parse(res)
|
||||
asserts.NoError(err)
|
||||
query := resURL.Query()
|
||||
asserts.NotEmpty(query.Get("Signature"))
|
||||
asserts.NotEmpty(query.Get("Expires"))
|
||||
asserts.Equal("ak", query.Get("OSSAccessKeyId"))
|
||||
asserts.EqualValues("819200", query.Get("x-oss-traffic-limit"))
|
||||
asserts.NotEmpty(query.Get("response-content-disposition"))
|
||||
}
|
||||
|
||||
// 限速超出范围 + 下载
|
||||
{
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, model.File{Name: "123.txt"})
|
||||
res, err := handler.Source(ctx, "/123", url.URL{}, 10, true, 838860801)
|
||||
asserts.NoError(err)
|
||||
resURL, err := url.Parse(res)
|
||||
asserts.NoError(err)
|
||||
query := resURL.Query()
|
||||
asserts.NotEmpty(query.Get("Signature"))
|
||||
asserts.NotEmpty(query.Get("Expires"))
|
||||
asserts.Equal("ak", query.Get("OSSAccessKeyId"))
|
||||
asserts.EqualValues("838860800", query.Get("x-oss-traffic-limit"))
|
||||
asserts.NotEmpty(query.Get("response-content-disposition"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_Thumb(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "test.com",
|
||||
},
|
||||
}
|
||||
|
||||
// 上下文不存在
|
||||
{
|
||||
ctx := context.Background()
|
||||
res, err := handler.Thumb(ctx, "/123.txt")
|
||||
asserts.Error(err)
|
||||
asserts.Nil(res)
|
||||
}
|
||||
|
||||
// 成功
|
||||
{
|
||||
cache.Set("setting_preview_timeout", "60", 0)
|
||||
ctx := context.WithValue(context.Background(), fsctx.ThumbSizeCtx, [2]uint{10, 20})
|
||||
res, err := handler.Thumb(ctx, "/123.jpg")
|
||||
asserts.NoError(err)
|
||||
resURL, err := url.Parse(res.URL)
|
||||
asserts.NoError(err)
|
||||
urlQuery := resURL.Query()
|
||||
asserts.Equal("image/resize,m_lfit,h_20,w_10", urlQuery.Get("x-oss-process"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_Delete(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "oss-cn-shanghai.aliyuncs.com",
|
||||
},
|
||||
}
|
||||
|
||||
// 失败
|
||||
{
|
||||
res, err := handler.Delete(context.Background(), []string{"1", "2", "3"})
|
||||
asserts.Error(err)
|
||||
asserts.Equal([]string{"1", "2", "3"}, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_Put(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "oss-cn-shanghai.aliyuncs.com",
|
||||
},
|
||||
}
|
||||
cache.Set("setting_upload_credential_timeout", "3600", 0)
|
||||
|
||||
// 失败
|
||||
{
|
||||
err := handler.Put(context.Background(), ioutil.NopCloser(strings.NewReader("123")), "/123.txt", 3)
|
||||
asserts.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type ClientMock struct {
|
||||
testMock.Mock
|
||||
}
|
||||
|
||||
func (m ClientMock) Request(method, target string, body io.Reader, opts ...request.Option) *request.Response {
|
||||
args := m.Called(method, target, body, opts)
|
||||
return args.Get(0).(*request.Response)
|
||||
}
|
||||
|
||||
func TestDriver_Get(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
AccessKey: "ak",
|
||||
SecretKey: "sk",
|
||||
BucketName: "test",
|
||||
Server: "oss-cn-shanghai.aliyuncs.com",
|
||||
},
|
||||
HTTPClient: request.HTTPClient{},
|
||||
}
|
||||
cache.Set("setting_preview_timeout", "3600", 0)
|
||||
|
||||
// 响应失败
|
||||
{
|
||||
res, err := handler.Get(context.Background(), "123.txt")
|
||||
asserts.Error(err)
|
||||
asserts.Nil(res)
|
||||
}
|
||||
|
||||
// 响应成功
|
||||
{
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, model.File{Size: 3})
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"GET",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`123`)),
|
||||
},
|
||||
})
|
||||
handler.HTTPClient = clientMock
|
||||
res, err := handler.Get(ctx, "123.txt")
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.NoError(err)
|
||||
n, err := res.Seek(0, io.SeekEnd)
|
||||
asserts.NoError(err)
|
||||
asserts.EqualValues(3, n)
|
||||
content, err := ioutil.ReadAll(res)
|
||||
asserts.NoError(err)
|
||||
asserts.Equal("123", string(content))
|
||||
}
|
||||
}
|
||||
335
pkg/filesystem/driver/oss/handller.go
Normal file
335
pkg/filesystem/driver/oss/handller.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package oss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/response"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UploadPolicy 阿里云OSS上传策略
|
||||
type UploadPolicy struct {
|
||||
Expiration string `json:"expiration"`
|
||||
Conditions []interface{} `json:"conditions"`
|
||||
}
|
||||
|
||||
// CallbackPolicy 回调策略
|
||||
type CallbackPolicy struct {
|
||||
CallbackURL string `json:"callbackUrl"`
|
||||
CallbackBody string `json:"callbackBody"`
|
||||
CallbackBodyType string `json:"callbackBodyType"`
|
||||
}
|
||||
|
||||
// Driver 阿里云OSS策略适配器
|
||||
type Driver struct {
|
||||
Policy *model.Policy
|
||||
client *oss.Client
|
||||
bucket *oss.Bucket
|
||||
HTTPClient request.Client
|
||||
}
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
// VersionID 文件版本标识
|
||||
VersionID key = iota
|
||||
)
|
||||
|
||||
// InitOSSClient 初始化OSS鉴权客户端
|
||||
func (handler *Driver) InitOSSClient() error {
|
||||
if handler.Policy == nil {
|
||||
return errors.New("存储策略为空")
|
||||
}
|
||||
|
||||
if handler.client == nil {
|
||||
// 初始化客户端
|
||||
client, err := oss.New(handler.Policy.Server, handler.Policy.AccessKey, handler.Policy.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler.client = client
|
||||
|
||||
// 初始化存储桶
|
||||
bucket, err := client.Bucket(handler.Policy.BucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler.bucket = bucket
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取文件
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
// 通过VersionID禁止缓存
|
||||
ctx = context.WithValue(ctx, VersionID, time.Now().UnixNano())
|
||||
|
||||
// 获取文件源地址
|
||||
downloadURL, err := handler.Source(
|
||||
ctx,
|
||||
path,
|
||||
url.URL{},
|
||||
int64(model.GetIntSetting("preview_timeout", 60)),
|
||||
false,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取文件数据流
|
||||
resp, err := handler.HTTPClient.Request(
|
||||
"GET",
|
||||
downloadURL,
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
).CheckHTTPResponse(200).GetRSCloser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.SetFirstFakeChunk()
|
||||
|
||||
// 尝试自主获取文件大小
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
resp.SetContentLength(int64(file.Size))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
defer file.Close()
|
||||
|
||||
// 初始化客户端
|
||||
if err := handler.InitOSSClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 凭证有效期
|
||||
credentialTTL := model.GetIntSetting("upload_credential_timeout", 3600)
|
||||
|
||||
options := []oss.Option{
|
||||
oss.Expires(time.Now().Add(time.Duration(credentialTTL) * time.Second)),
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
err := handler.bucket.PutObject(dst, file, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除一个或多个文件,
|
||||
// 返回未删除的文件
|
||||
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
// 初始化客户端
|
||||
if err := handler.InitOSSClient(); err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
delRes, err := handler.bucket.DeleteObjects(files)
|
||||
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
// 统计未删除的文件
|
||||
failed := util.SliceDifference(files, delRes.DeletedObjects)
|
||||
if len(failed) > 0 {
|
||||
return failed, errors.New("删除失败")
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
// 初始化客户端
|
||||
if err := handler.InitOSSClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
thumbSize = [2]uint{400, 300}
|
||||
ok = false
|
||||
)
|
||||
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
|
||||
return nil, errors.New("无法获取缩略图尺寸设置")
|
||||
}
|
||||
|
||||
thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d", thumbSize[1], thumbSize[0])
|
||||
thumbOption := []oss.Option{oss.Process(thumbParam)}
|
||||
thumbURL, err := handler.signSourceURL(
|
||||
ctx,
|
||||
path,
|
||||
int64(model.GetIntSetting("preview_timeout", 60)),
|
||||
thumbOption,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.ContentResponse{
|
||||
Redirect: true,
|
||||
URL: thumbURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Source 获取外链URL
|
||||
func (handler Driver) Source(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
baseURL url.URL,
|
||||
ttl int64,
|
||||
isDownload bool,
|
||||
speed int,
|
||||
) (string, error) {
|
||||
// 初始化客户端
|
||||
if err := handler.InitOSSClient(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 尝试从上下文获取文件名
|
||||
fileName := ""
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
fileName = file.Name
|
||||
}
|
||||
|
||||
// 添加各项设置
|
||||
var signOptions = make([]oss.Option, 0, 2)
|
||||
if isDownload {
|
||||
signOptions = append(signOptions, oss.ResponseContentDisposition("attachment; filename=\""+url.PathEscape(fileName)+"\""))
|
||||
}
|
||||
if speed > 0 {
|
||||
// OSS对速度值有范围限制
|
||||
if speed < 819200 {
|
||||
speed = 819200
|
||||
}
|
||||
if speed > 838860800 {
|
||||
speed = 838860800
|
||||
}
|
||||
signOptions = append(signOptions, oss.TrafficLimitParam(int64(speed)))
|
||||
}
|
||||
|
||||
return handler.signSourceURL(ctx, path, ttl, signOptions)
|
||||
}
|
||||
|
||||
func (handler Driver) signSourceURL(ctx context.Context, path string, ttl int64, options []oss.Option) (string, error) {
|
||||
// 是否带有 Version ID
|
||||
if _, ok := ctx.Value(VersionID).(int64); ok {
|
||||
|
||||
}
|
||||
signedURL, err := handler.bucket.SignURL(path, oss.HTTPGet, ttl, options...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将最终生成的签名URL域名换成用户自定义的加速域名(如果有)
|
||||
finalURL, err := url.Parse(signedURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cdnURL, err := url.Parse(handler.Policy.BaseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
finalURL.Host = cdnURL.Host
|
||||
finalURL.Scheme = cdnURL.Scheme
|
||||
|
||||
return finalURL.String(), nil
|
||||
}
|
||||
|
||||
// Token 获取上传策略和认证Token
|
||||
func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) {
|
||||
// 读取上下文中生成的存储路径
|
||||
savePath, ok := ctx.Value(fsctx.SavePathCtx).(string)
|
||||
if !ok {
|
||||
return serializer.UploadCredential{}, errors.New("无法获取存储路径")
|
||||
}
|
||||
|
||||
// 生成回调地址
|
||||
siteURL := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/callback/oss/" + key)
|
||||
apiURL := siteURL.ResolveReference(apiBaseURI)
|
||||
|
||||
// 回调策略
|
||||
callbackPolicy := CallbackPolicy{
|
||||
CallbackURL: apiURL.String(),
|
||||
CallbackBody: `{"name":${x:fname},"source_name":${object},"size":${size},"pic_info":"${imageInfo.width},${imageInfo.height}"}`,
|
||||
CallbackBodyType: "application/json",
|
||||
}
|
||||
|
||||
// 上传策略
|
||||
postPolicy := UploadPolicy{
|
||||
Expiration: time.Now().UTC().Add(time.Duration(TTL) * time.Second).Format(time.RFC3339),
|
||||
Conditions: []interface{}{
|
||||
map[string]string{"bucket": handler.Policy.BucketName},
|
||||
[]string{"starts-with", "$key", path.Dir(savePath)},
|
||||
[]interface{}{"content-length-range", 0, handler.Policy.MaxSize},
|
||||
},
|
||||
}
|
||||
|
||||
return handler.getUploadCredential(ctx, postPolicy, callbackPolicy, TTL)
|
||||
}
|
||||
|
||||
func (handler Driver) getUploadCredential(ctx context.Context, policy UploadPolicy, callback CallbackPolicy, TTL int64) (serializer.UploadCredential, error) {
|
||||
// 读取上下文中生成的存储路径
|
||||
savePath, ok := ctx.Value(fsctx.SavePathCtx).(string)
|
||||
if !ok {
|
||||
return serializer.UploadCredential{}, errors.New("无法获取存储路径")
|
||||
}
|
||||
|
||||
// 处理回调策略
|
||||
callbackPolicyEncoded := ""
|
||||
if callback.CallbackURL != "" {
|
||||
callbackPolicyJSON, err := json.Marshal(callback)
|
||||
if err != nil {
|
||||
return serializer.UploadCredential{}, err
|
||||
}
|
||||
callbackPolicyEncoded = base64.StdEncoding.EncodeToString(callbackPolicyJSON)
|
||||
policy.Conditions = append(policy.Conditions, map[string]string{"callback": callbackPolicyEncoded})
|
||||
}
|
||||
|
||||
// 编码上传策略
|
||||
policyJSON, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return serializer.UploadCredential{}, err
|
||||
}
|
||||
policyEncoded := base64.StdEncoding.EncodeToString(policyJSON)
|
||||
|
||||
// 签名上传策略
|
||||
hmacSign := hmac.New(sha1.New, []byte(handler.Policy.SecretKey))
|
||||
_, err = io.WriteString(hmacSign, policyEncoded)
|
||||
if err != nil {
|
||||
return serializer.UploadCredential{}, err
|
||||
}
|
||||
signature := base64.StdEncoding.EncodeToString(hmacSign.Sum(nil))
|
||||
|
||||
return serializer.UploadCredential{
|
||||
Policy: fmt.Sprintf("%s:%s", callbackPolicyEncoded, policyEncoded),
|
||||
Path: savePath,
|
||||
AccessKey: handler.Policy.AccessKey,
|
||||
Token: signature,
|
||||
}, nil
|
||||
}
|
||||
238
pkg/filesystem/driver/qiniu/handller.go
Normal file
238
pkg/filesystem/driver/qiniu/handller.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package qiniu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/response"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/qiniu/api.v7/v7/auth/qbox"
|
||||
"github.com/qiniu/api.v7/v7/storage"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Driver 本地策略适配器
|
||||
type Driver struct {
|
||||
Policy *model.Policy
|
||||
}
|
||||
|
||||
// Get 获取文件
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
// 给文件名加上随机参数以强制拉取
|
||||
path = fmt.Sprintf("%s?v=%d", path, time.Now().UnixNano())
|
||||
|
||||
// 获取文件源地址
|
||||
downloadURL, err := handler.Source(
|
||||
ctx,
|
||||
path,
|
||||
url.URL{},
|
||||
int64(model.GetIntSetting("preview_timeout", 60)),
|
||||
false,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取文件数据流
|
||||
client := request.HTTPClient{}
|
||||
resp, err := client.Request(
|
||||
"GET",
|
||||
downloadURL,
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
request.WithHeader(
|
||||
http.Header{"Cache-Control": {"no-cache", "no-store", "must-revalidate"}},
|
||||
),
|
||||
).CheckHTTPResponse(200).GetRSCloser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.SetFirstFakeChunk()
|
||||
|
||||
// 尝试自主获取文件大小
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
resp.SetContentLength(int64(file.Size))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
defer file.Close()
|
||||
|
||||
// 凭证有效期
|
||||
credentialTTL := model.GetIntSetting("upload_credential_timeout", 3600)
|
||||
|
||||
// 生成上传策略
|
||||
putPolicy := storage.PutPolicy{
|
||||
// 指定为覆盖策略
|
||||
Scope: fmt.Sprintf("%s:%s", handler.Policy.BucketName, dst),
|
||||
SaveKey: dst,
|
||||
ForceSaveKey: true,
|
||||
FsizeLimit: int64(size),
|
||||
}
|
||||
// 是否开启了MIMEType限制
|
||||
if handler.Policy.OptionsSerialized.MimeType != "" {
|
||||
putPolicy.MimeLimit = handler.Policy.OptionsSerialized.MimeType
|
||||
}
|
||||
|
||||
// 生成上传凭证
|
||||
token, err := handler.getUploadCredential(ctx, putPolicy, int64(credentialTTL))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建上传表单
|
||||
cfg := storage.Config{}
|
||||
formUploader := storage.NewFormUploader(&cfg)
|
||||
ret := storage.PutRet{}
|
||||
putExtra := storage.PutExtra{
|
||||
Params: map[string]string{},
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
err = formUploader.Put(ctx, &ret, token.Token, dst, file, int64(size), &putExtra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除一个或多个文件,
|
||||
// 返回未删除的文件
|
||||
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
// TODO 大于一千个文件需要分批发送
|
||||
deleteOps := make([]string, 0, len(files))
|
||||
for _, key := range files {
|
||||
deleteOps = append(deleteOps, storage.URIDelete(handler.Policy.BucketName, key))
|
||||
}
|
||||
|
||||
mac := qbox.NewMac(handler.Policy.AccessKey, handler.Policy.SecretKey)
|
||||
cfg := storage.Config{
|
||||
UseHTTPS: true,
|
||||
}
|
||||
bucketManager := storage.NewBucketManager(mac, &cfg)
|
||||
rets, err := bucketManager.Batch(deleteOps)
|
||||
|
||||
// 处理删除结果
|
||||
if err != nil {
|
||||
failed := make([]string, 0, len(rets))
|
||||
for k, ret := range rets {
|
||||
if ret.Code != 200 {
|
||||
failed = append(failed, files[k])
|
||||
}
|
||||
}
|
||||
return failed, errors.New("删除失败")
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
var (
|
||||
thumbSize = [2]uint{400, 300}
|
||||
ok = false
|
||||
)
|
||||
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
|
||||
return nil, errors.New("无法获取缩略图尺寸设置")
|
||||
}
|
||||
|
||||
path = fmt.Sprintf("%s?imageView2/1/w/%d/h/%d", path, thumbSize[0], thumbSize[1])
|
||||
return &response.ContentResponse{
|
||||
Redirect: true,
|
||||
URL: handler.signSourceURL(
|
||||
ctx,
|
||||
path,
|
||||
int64(model.GetIntSetting("preview_timeout", 60)),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Source 获取外链URL
|
||||
func (handler Driver) Source(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
baseURL url.URL,
|
||||
ttl int64,
|
||||
isDownload bool,
|
||||
speed int,
|
||||
) (string, error) {
|
||||
// 尝试从上下文获取文件名
|
||||
fileName := ""
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
fileName = file.Name
|
||||
}
|
||||
|
||||
// 加入下载相关设置
|
||||
if isDownload {
|
||||
path = path + "?attname=" + url.PathEscape(fileName)
|
||||
}
|
||||
|
||||
// 取得原始文件地址
|
||||
return handler.signSourceURL(ctx, path, ttl), nil
|
||||
}
|
||||
|
||||
func (handler Driver) signSourceURL(ctx context.Context, path string, ttl int64) string {
|
||||
var sourceURL string
|
||||
if handler.Policy.IsPrivate {
|
||||
mac := qbox.NewMac(handler.Policy.AccessKey, handler.Policy.SecretKey)
|
||||
deadline := time.Now().Add(time.Second * time.Duration(ttl)).Unix()
|
||||
sourceURL = storage.MakePrivateURL(mac, handler.Policy.BaseURL, path, deadline)
|
||||
} else {
|
||||
sourceURL = storage.MakePublicURL(handler.Policy.BaseURL, path)
|
||||
}
|
||||
return sourceURL
|
||||
}
|
||||
|
||||
// Token 获取上传策略和认证Token
|
||||
func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) {
|
||||
// 生成回调地址
|
||||
siteURL := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/callback/qiniu/" + key)
|
||||
apiURL := siteURL.ResolveReference(apiBaseURI)
|
||||
|
||||
// 读取上下文中生成的存储路径
|
||||
savePath, ok := ctx.Value(fsctx.SavePathCtx).(string)
|
||||
if !ok {
|
||||
return serializer.UploadCredential{}, errors.New("无法获取存储路径")
|
||||
}
|
||||
|
||||
// 创建上传策略
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: handler.Policy.BucketName,
|
||||
CallbackURL: apiURL.String(),
|
||||
CallbackBody: `{"name":"$(fname)","source_name":"$(key)","size":$(fsize),"pic_info":"$(imageInfo.width),$(imageInfo.height)"}`,
|
||||
CallbackBodyType: "application/json",
|
||||
SaveKey: savePath,
|
||||
ForceSaveKey: true,
|
||||
FsizeLimit: int64(handler.Policy.MaxSize),
|
||||
}
|
||||
// 是否开启了MIMEType限制
|
||||
if handler.Policy.OptionsSerialized.MimeType != "" {
|
||||
putPolicy.MimeLimit = handler.Policy.OptionsSerialized.MimeType
|
||||
}
|
||||
|
||||
return handler.getUploadCredential(ctx, putPolicy, TTL)
|
||||
}
|
||||
|
||||
// getUploadCredential 签名上传策略
|
||||
func (handler Driver) getUploadCredential(ctx context.Context, policy storage.PutPolicy, TTL int64) (serializer.UploadCredential, error) {
|
||||
policy.Expires = uint64(TTL)
|
||||
mac := qbox.NewMac(handler.Policy.AccessKey, handler.Policy.SecretKey)
|
||||
upToken := policy.UploadToken(mac)
|
||||
|
||||
return serializer.UploadCredential{
|
||||
Token: upToken,
|
||||
}, nil
|
||||
}
|
||||
279
pkg/filesystem/driver/remote/handler.go
Normal file
279
pkg/filesystem/driver/remote/handler.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/auth"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/response"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Driver 远程存储策略适配器
|
||||
type Driver struct {
|
||||
Client request.Client
|
||||
Policy *model.Policy
|
||||
AuthInstance auth.Auth
|
||||
}
|
||||
|
||||
// getAPIUrl 获取接口请求地址
|
||||
func (handler Driver) getAPIUrl(scope string, routes ...string) string {
|
||||
serverURL, err := url.Parse(handler.Policy.Server)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var controller *url.URL
|
||||
|
||||
switch scope {
|
||||
case "delete":
|
||||
controller, _ = url.Parse("/api/v3/slave/delete")
|
||||
case "thumb":
|
||||
controller, _ = url.Parse("/api/v3/slave/thumb")
|
||||
default:
|
||||
controller = serverURL
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
controller.Path = path.Join(controller.Path, r)
|
||||
}
|
||||
|
||||
return serverURL.ResolveReference(controller).String()
|
||||
}
|
||||
|
||||
// Get 获取文件内容
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
// 尝试获取速度限制 TODO 是否需要在这里限制?
|
||||
speedLimit := 0
|
||||
if user, ok := ctx.Value(fsctx.UserCtx).(model.User); ok {
|
||||
speedLimit = user.Group.SpeedLimit
|
||||
}
|
||||
|
||||
// 获取文件源地址
|
||||
downloadURL, err := handler.Source(ctx, path, url.URL{}, 0, true, speedLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取文件数据流
|
||||
resp, err := handler.Client.Request(
|
||||
"GET",
|
||||
downloadURL,
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
).CheckHTTPResponse(200).GetRSCloser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.SetFirstFakeChunk()
|
||||
|
||||
// 尝试获取文件大小
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
resp.SetContentLength(int64(file.Size))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
defer file.Close()
|
||||
|
||||
// 凭证有效期
|
||||
credentialTTL := model.GetIntSetting("upload_credential_timeout", 3600)
|
||||
|
||||
// 生成上传策略
|
||||
policy := serializer.UploadPolicy{
|
||||
SavePath: path.Dir(dst),
|
||||
FileName: path.Base(dst),
|
||||
AutoRename: false,
|
||||
MaxSize: size,
|
||||
}
|
||||
credential, err := handler.getUploadCredential(ctx, policy, int64(credentialTTL))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 对文件名进行URLEncode
|
||||
fileName, err := url.QueryUnescape(path.Base(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 上传文件
|
||||
resp, err := handler.Client.Request(
|
||||
"POST",
|
||||
handler.Policy.GetUploadURL(),
|
||||
file,
|
||||
request.WithHeader(map[string][]string{
|
||||
"Authorization": {credential.Token},
|
||||
"X-Policy": {credential.Policy},
|
||||
"X-FileName": {fileName},
|
||||
}),
|
||||
request.WithContentLength(int64(size)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除一个或多个文件,
|
||||
// 返回未删除的文件,及遇到的最后一个错误
|
||||
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
// 封装接口请求正文
|
||||
reqBody := serializer.RemoteDeleteRequest{
|
||||
Files: files,
|
||||
}
|
||||
reqBodyEncoded, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
// 发送删除请求
|
||||
bodyReader := strings.NewReader(string(reqBodyEncoded))
|
||||
signTTL := model.GetIntSetting("slave_api_timeout", 60)
|
||||
resp, err := handler.Client.Request(
|
||||
"POST",
|
||||
handler.getAPIUrl("delete"),
|
||||
bodyReader,
|
||||
request.WithCredential(handler.AuthInstance, int64(signTTL)),
|
||||
).CheckHTTPResponse(200).GetResponse()
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
// 处理删除结果
|
||||
var reqResp serializer.Response
|
||||
err = json.Unmarshal([]byte(resp), &reqResp)
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
if reqResp.Code != 0 {
|
||||
var failedResp serializer.RemoteDeleteRequest
|
||||
if failed, ok := reqResp.Data.(string); ok {
|
||||
err = json.Unmarshal([]byte(failed), &failedResp)
|
||||
if err == nil {
|
||||
return failedResp.Files, errors.New(reqResp.Error)
|
||||
}
|
||||
}
|
||||
return files, errors.New("未知的返回结果格式")
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
sourcePath := base64.RawURLEncoding.EncodeToString([]byte(path))
|
||||
thumbURL := handler.getAPIUrl("thumb") + "/" + sourcePath
|
||||
ttl := model.GetIntSetting("preview_timeout", 60)
|
||||
signedThumbURL, err := auth.SignURI(handler.AuthInstance, thumbURL, int64(ttl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.ContentResponse{
|
||||
Redirect: true,
|
||||
URL: signedThumbURL.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Source 获取外链URL
|
||||
func (handler Driver) Source(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
baseURL url.URL,
|
||||
ttl int64,
|
||||
isDownload bool,
|
||||
speed int,
|
||||
) (string, error) {
|
||||
// 尝试从上下文获取文件名
|
||||
fileName := "file"
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
fileName = file.Name
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(handler.Policy.Server)
|
||||
if err != nil {
|
||||
return "", errors.New("无法解析远程服务端地址")
|
||||
}
|
||||
|
||||
var (
|
||||
signedURI *url.URL
|
||||
controller = "/api/v3/slave/download"
|
||||
)
|
||||
if !isDownload {
|
||||
controller = "/api/v3/slave/source"
|
||||
}
|
||||
|
||||
// 签名下载地址
|
||||
sourcePath := base64.RawURLEncoding.EncodeToString([]byte(path))
|
||||
signedURI, err = auth.SignURI(
|
||||
handler.AuthInstance,
|
||||
fmt.Sprintf("%s/%d/%s/%s", controller, speed, sourcePath, fileName),
|
||||
ttl,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeEncryptError, "无法对URL进行签名", err)
|
||||
}
|
||||
|
||||
finalURL := serverURL.ResolveReference(signedURI).String()
|
||||
return finalURL, nil
|
||||
|
||||
}
|
||||
|
||||
// Token 获取上传策略和认证Token
|
||||
func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) {
|
||||
// 生成回调地址
|
||||
siteURL := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/callback/remote/" + key)
|
||||
apiURL := siteURL.ResolveReference(apiBaseURI)
|
||||
|
||||
// 生成上传策略
|
||||
policy := serializer.UploadPolicy{
|
||||
SavePath: handler.Policy.DirNameRule,
|
||||
FileName: handler.Policy.FileNameRule,
|
||||
AutoRename: handler.Policy.AutoRename,
|
||||
MaxSize: handler.Policy.MaxSize,
|
||||
AllowedExtension: handler.Policy.OptionsSerialized.FileType,
|
||||
CallbackURL: apiURL.String(),
|
||||
}
|
||||
return handler.getUploadCredential(ctx, policy, TTL)
|
||||
}
|
||||
|
||||
func (handler Driver) getUploadCredential(ctx context.Context, policy serializer.UploadPolicy, TTL int64) (serializer.UploadCredential, error) {
|
||||
policyEncoded, err := policy.EncodeUploadPolicy()
|
||||
if err != nil {
|
||||
return serializer.UploadCredential{}, err
|
||||
}
|
||||
|
||||
// 签名上传策略
|
||||
uploadRequest, _ := http.NewRequest("POST", "/api/v3/slave/upload", nil)
|
||||
uploadRequest.Header = map[string][]string{
|
||||
"X-Policy": {policyEncoded},
|
||||
}
|
||||
auth.SignRequest(handler.AuthInstance, uploadRequest, TTL)
|
||||
|
||||
if credential, ok := uploadRequest.Header["Authorization"]; ok && len(credential) == 1 {
|
||||
return serializer.UploadCredential{
|
||||
Token: credential[0],
|
||||
Policy: policyEncoded,
|
||||
}, nil
|
||||
}
|
||||
return serializer.UploadCredential{}, errors.New("无法签名上传策略")
|
||||
}
|
||||
354
pkg/filesystem/driver/remote/handler_test.go
Normal file
354
pkg/filesystem/driver/remote/handler_test.go
Normal file
@@ -0,0 +1,354 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/auth"
|
||||
"github.com/HFO4/cloudreve/pkg/cache"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
testMock "github.com/stretchr/testify/mock"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandler_Token(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
MaxSize: 10,
|
||||
AutoRename: true,
|
||||
DirNameRule: "dir",
|
||||
FileNameRule: "file",
|
||||
OptionsSerialized: model.PolicyOption{
|
||||
FileType: []string{"txt"},
|
||||
},
|
||||
Server: "http://test.com",
|
||||
},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
ctx := context.Background()
|
||||
auth.General = auth.HMACAuth{SecretKey: []byte("test")}
|
||||
|
||||
// 成功
|
||||
{
|
||||
cache.Set("setting_siteURL", "http://test.cloudreve.org", 0)
|
||||
credential, err := handler.Token(ctx, 10, "123")
|
||||
asserts.NoError(err)
|
||||
policy, err := serializer.DecodeUploadPolicy(credential.Policy)
|
||||
asserts.NoError(err)
|
||||
asserts.Equal("http://test.cloudreve.org/api/v3/callback/remote/123", policy.CallbackURL)
|
||||
asserts.Equal(uint64(10), policy.MaxSize)
|
||||
asserts.Equal(true, policy.AutoRename)
|
||||
asserts.Equal("dir", policy.SavePath)
|
||||
asserts.Equal("file", policy.FileName)
|
||||
asserts.Equal([]string{"txt"}, policy.AllowedExtension)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHandler_Source(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
auth.General = auth.HMACAuth{SecretKey: []byte("test")}
|
||||
|
||||
// 无法获取上下文
|
||||
{
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{Server: "/"},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
ctx := context.Background()
|
||||
res, err := handler.Source(ctx, "", url.URL{}, 0, true, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.NotEmpty(res)
|
||||
}
|
||||
|
||||
// 成功
|
||||
{
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{Server: "/"},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
file := model.File{
|
||||
SourceName: "1.txt",
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
|
||||
res, err := handler.Source(ctx, "", url.URL{}, 10, true, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.Contains(res, "api/v3/slave/download/0")
|
||||
}
|
||||
|
||||
// 成功 预览
|
||||
{
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{Server: "/"},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
file := model.File{
|
||||
SourceName: "1.txt",
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
|
||||
res, err := handler.Source(ctx, "", url.URL{}, 10, false, 0)
|
||||
asserts.NoError(err)
|
||||
asserts.Contains(res, "api/v3/slave/source/0")
|
||||
}
|
||||
}
|
||||
|
||||
type ClientMock struct {
|
||||
testMock.Mock
|
||||
}
|
||||
|
||||
func (m ClientMock) Request(method, target string, body io.Reader, opts ...request.Option) *request.Response {
|
||||
args := m.Called(method, target, body, opts)
|
||||
return args.Get(0).(*request.Response)
|
||||
}
|
||||
|
||||
func TestHandler_Delete(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
SecretKey: "test",
|
||||
Server: "http://test.com",
|
||||
},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
ctx := context.Background()
|
||||
cache.Set("setting_slave_api_timeout", "60", 0)
|
||||
|
||||
// 成功
|
||||
{
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"POST",
|
||||
"http://test.com/api/v3/slave/delete",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":0}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
failed, err := handler.Delete(ctx, []string{"/test1.txt", "test2.txt"})
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.NoError(err)
|
||||
asserts.Len(failed, 0)
|
||||
|
||||
}
|
||||
|
||||
// 结果解析失败
|
||||
{
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"POST",
|
||||
"http://test.com/api/v3/slave/delete",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":203}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
failed, err := handler.Delete(ctx, []string{"/test1.txt", "test2.txt"})
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.Error(err)
|
||||
asserts.Len(failed, 2)
|
||||
}
|
||||
|
||||
// 一个失败
|
||||
{
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"POST",
|
||||
"http://test.com/api/v3/slave/delete",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":203,"data":"{\"files\":[\"1\"]}"}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
failed, err := handler.Delete(ctx, []string{"/test1.txt", "test2.txt"})
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.Error(err)
|
||||
asserts.Len(failed, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Get(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
SecretKey: "test",
|
||||
Server: "http://test.com",
|
||||
},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
// 成功
|
||||
{
|
||||
ctx = context.WithValue(ctx, fsctx.UserCtx, model.User{})
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"GET",
|
||||
testMock.Anything,
|
||||
nil,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":0}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
resp, err := handler.Get(ctx, "/test.txt")
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.NotNil(resp)
|
||||
asserts.NoError(err)
|
||||
}
|
||||
|
||||
// 请求失败
|
||||
{
|
||||
ctx = context.WithValue(ctx, fsctx.UserCtx, model.User{})
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"GET",
|
||||
testMock.Anything,
|
||||
nil,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 404,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":0}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
resp, err := handler.Get(ctx, "/test.txt")
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.Nil(resp)
|
||||
asserts.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_Put(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
Type: "remote",
|
||||
SecretKey: "test",
|
||||
Server: "http://test.com",
|
||||
},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
ctx := context.Background()
|
||||
asserts.NoError(cache.Set("setting_upload_credential_timeout", "3600", 0))
|
||||
|
||||
// 成功
|
||||
{
|
||||
ctx = context.WithValue(ctx, fsctx.UserCtx, model.User{})
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"POST",
|
||||
"http://test.com/api/v3/slave/upload",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":0}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
err := handler.Put(ctx, ioutil.NopCloser(strings.NewReader("test input file")), "/", 15)
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.NoError(err)
|
||||
}
|
||||
|
||||
// 请求失败
|
||||
{
|
||||
ctx = context.WithValue(ctx, fsctx.UserCtx, model.User{})
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"POST",
|
||||
"http://test.com/api/v3/slave/upload",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 404,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":0}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
err := handler.Put(ctx, ioutil.NopCloser(strings.NewReader("test input file")), "/", 15)
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.Error(err)
|
||||
}
|
||||
|
||||
// 返回错误
|
||||
{
|
||||
ctx = context.WithValue(ctx, fsctx.UserCtx, model.User{})
|
||||
clientMock := ClientMock{}
|
||||
clientMock.On(
|
||||
"Request",
|
||||
"POST",
|
||||
"http://test.com/api/v3/slave/upload",
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: nil,
|
||||
Response: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"code":1}`)),
|
||||
},
|
||||
})
|
||||
handler.Client = clientMock
|
||||
err := handler.Put(ctx, ioutil.NopCloser(strings.NewReader("test input file")), "/", 15)
|
||||
clientMock.AssertExpectations(t)
|
||||
asserts.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHandler_Thumb(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
handler := Driver{
|
||||
Policy: &model.Policy{
|
||||
Type: "remote",
|
||||
SecretKey: "test",
|
||||
Server: "http://test.com",
|
||||
},
|
||||
AuthInstance: auth.HMACAuth{},
|
||||
}
|
||||
ctx := context.Background()
|
||||
asserts.NoError(cache.Set("setting_preview_timeout", "60", 0))
|
||||
resp, err := handler.Thumb(ctx, "/1.txt")
|
||||
asserts.NoError(err)
|
||||
asserts.True(resp.Redirect)
|
||||
}
|
||||
54
pkg/filesystem/driver/template/handller.go
Normal file
54
pkg/filesystem/driver/template/handller.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/response"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"io"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Driver 适配器模板
|
||||
type Driver struct {
|
||||
Policy *model.Policy
|
||||
}
|
||||
|
||||
// Get 获取文件
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
return nil, errors.New("未实现")
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
return errors.New("未实现")
|
||||
}
|
||||
|
||||
// Delete 删除一个或多个文件,
|
||||
// 返回未删除的文件,及遇到的最后一个错误
|
||||
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
return []string{}, errors.New("未实现")
|
||||
}
|
||||
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
return nil, errors.New("未实现")
|
||||
}
|
||||
|
||||
// Source 获取外链URL
|
||||
func (handler Driver) Source(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
baseURL url.URL,
|
||||
ttl int64,
|
||||
isDownload bool,
|
||||
speed int,
|
||||
) (string, error) {
|
||||
return "", errors.New("未实现")
|
||||
}
|
||||
|
||||
// Token 获取上传策略和认证Token
|
||||
func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) {
|
||||
return serializer.UploadCredential{}, errors.New("未实现")
|
||||
}
|
||||
258
pkg/filesystem/driver/upyun/handller.go
Normal file
258
pkg/filesystem/driver/upyun/handller.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package upyun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/response"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/upyun/go-sdk/upyun"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UploadPolicy 又拍云上传策略
|
||||
type UploadPolicy struct {
|
||||
Bucket string `json:"bucket"`
|
||||
SaveKey string `json:"save-key"`
|
||||
Expiration int64 `json:"expiration"`
|
||||
CallbackURL string `json:"notify-url"`
|
||||
ContentLength uint64 `json:"content-length"`
|
||||
ContentLengthRange string `json:"content-length-range"`
|
||||
AllowFileType string `json:"allow-file-type,omitempty"`
|
||||
}
|
||||
|
||||
// Driver 又拍云策略适配器
|
||||
type Driver struct {
|
||||
Policy *model.Policy
|
||||
}
|
||||
|
||||
// Get 获取文件
|
||||
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
|
||||
return nil, errors.New("未实现")
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
|
||||
return errors.New("未实现")
|
||||
}
|
||||
|
||||
// Delete 删除一个或多个文件,
|
||||
// 返回未删除的文件,及遇到的最后一个错误
|
||||
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
|
||||
up := upyun.NewUpYun(&upyun.UpYunConfig{
|
||||
Bucket: handler.Policy.BucketName,
|
||||
Operator: handler.Policy.AccessKey,
|
||||
Password: handler.Policy.SecretKey,
|
||||
})
|
||||
|
||||
var (
|
||||
failed = make([]string, 0, len(files))
|
||||
lastErr error
|
||||
currentIndex = 0
|
||||
indexLock sync.Mutex
|
||||
failedLock sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
routineNum = 4
|
||||
)
|
||||
wg.Add(routineNum)
|
||||
|
||||
// upyun不支持批量操作,这里开四个协程并行操作
|
||||
for i := 0; i < routineNum; i++ {
|
||||
go func() {
|
||||
for {
|
||||
// 取得待删除文件
|
||||
indexLock.Lock()
|
||||
if currentIndex >= len(files) {
|
||||
// 所有文件处理完成
|
||||
wg.Done()
|
||||
indexLock.Unlock()
|
||||
return
|
||||
}
|
||||
path := files[currentIndex]
|
||||
currentIndex++
|
||||
indexLock.Unlock()
|
||||
|
||||
// 发送异步删除请求
|
||||
err := up.Delete(&upyun.DeleteObjectConfig{
|
||||
Path: path,
|
||||
Async: true,
|
||||
})
|
||||
|
||||
// 处理错误
|
||||
if err != nil {
|
||||
failedLock.Lock()
|
||||
lastErr = err
|
||||
failed = append(failed, path)
|
||||
failedLock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return failed, lastErr
|
||||
}
|
||||
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
|
||||
var (
|
||||
thumbSize = [2]uint{400, 300}
|
||||
ok = false
|
||||
)
|
||||
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
|
||||
return nil, errors.New("无法获取缩略图尺寸设置")
|
||||
}
|
||||
|
||||
thumbParam := fmt.Sprintf("!/fwfh/%dx%d", thumbSize[0], thumbSize[1])
|
||||
thumbURL, err := handler.Source(
|
||||
ctx,
|
||||
path+thumbParam,
|
||||
url.URL{},
|
||||
int64(model.GetIntSetting("preview_timeout", 60)),
|
||||
false,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.ContentResponse{
|
||||
Redirect: true,
|
||||
URL: thumbURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Source 获取外链URL
|
||||
func (handler Driver) Source(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
baseURL url.URL,
|
||||
ttl int64,
|
||||
isDownload bool,
|
||||
speed int,
|
||||
) (string, error) {
|
||||
// 尝试从上下文获取文件名
|
||||
fileName := ""
|
||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||
fileName = file.Name
|
||||
}
|
||||
|
||||
sourceURL, err := url.Parse(handler.Policy.BaseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileKey, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sourceURL = sourceURL.ResolveReference(fileKey)
|
||||
|
||||
// 如果是下载文件URL
|
||||
if isDownload {
|
||||
query := sourceURL.Query()
|
||||
query.Add("_upd", fileName)
|
||||
sourceURL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
return handler.signURL(ctx, sourceURL, ttl)
|
||||
}
|
||||
|
||||
func (handler Driver) signURL(ctx context.Context, path *url.URL, TTL int64) (string, error) {
|
||||
if !handler.Policy.IsPrivate {
|
||||
// 未开启Token防盗链时,直接返回
|
||||
return path.String(), nil
|
||||
}
|
||||
|
||||
etime := time.Now().Add(time.Duration(TTL) * time.Second).Unix()
|
||||
signStr := fmt.Sprintf(
|
||||
"%s&%d&%s",
|
||||
handler.Policy.OptionsSerialized.Token,
|
||||
etime,
|
||||
path.Path,
|
||||
)
|
||||
signMd5 := fmt.Sprintf("%x", md5.Sum([]byte(signStr)))
|
||||
finalSign := signMd5[12:20] + strconv.FormatInt(etime, 10)
|
||||
|
||||
// 将签名添加到URL中
|
||||
query := path.Query()
|
||||
query.Add("_upt", finalSign)
|
||||
path.RawQuery = query.Encode()
|
||||
|
||||
return path.String(), nil
|
||||
}
|
||||
|
||||
// Token 获取上传策略和认证Token
|
||||
func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) {
|
||||
// 读取上下文中生成的存储路径和文件大小
|
||||
savePath, ok := ctx.Value(fsctx.SavePathCtx).(string)
|
||||
if !ok {
|
||||
return serializer.UploadCredential{}, errors.New("无法获取存储路径")
|
||||
}
|
||||
fileSize, ok := ctx.Value(fsctx.FileSizeCtx).(uint64)
|
||||
if !ok {
|
||||
return serializer.UploadCredential{}, errors.New("无法获取文件大小")
|
||||
}
|
||||
|
||||
// 生成回调地址
|
||||
siteURL := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/callback/upyun/" + key)
|
||||
apiURL := siteURL.ResolveReference(apiBaseURI)
|
||||
|
||||
// 上传策略
|
||||
putPolicy := UploadPolicy{
|
||||
Bucket: handler.Policy.BucketName,
|
||||
// TODO escape
|
||||
SaveKey: savePath,
|
||||
Expiration: time.Now().Add(time.Duration(TTL) * time.Second).Unix(),
|
||||
CallbackURL: apiURL.String(),
|
||||
ContentLength: fileSize,
|
||||
ContentLengthRange: fmt.Sprintf("0,%d", fileSize),
|
||||
AllowFileType: strings.Join(handler.Policy.OptionsSerialized.FileType, ","),
|
||||
}
|
||||
|
||||
// 生成上传凭证
|
||||
return handler.getUploadCredential(ctx, putPolicy)
|
||||
}
|
||||
|
||||
func (handler Driver) getUploadCredential(ctx context.Context, policy UploadPolicy) (serializer.UploadCredential, error) {
|
||||
// 生成上传策略
|
||||
policyJSON, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return serializer.UploadCredential{}, err
|
||||
}
|
||||
policyEncoded := base64.StdEncoding.EncodeToString(policyJSON)
|
||||
|
||||
// 生成签名
|
||||
elements := []string{"POST", "/" + handler.Policy.BucketName, policyEncoded}
|
||||
signStr := handler.Sign(ctx, elements)
|
||||
|
||||
return serializer.UploadCredential{
|
||||
Policy: policyEncoded,
|
||||
Token: signStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sign 计算又拍云的签名头
|
||||
func (handler Driver) Sign(ctx context.Context, elements []string) string {
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(handler.Policy.SecretKey)))
|
||||
mac := hmac.New(sha1.New, []byte(password))
|
||||
value := strings.Join(elements, "&")
|
||||
mac.Write([]byte(value))
|
||||
signStr := base64.StdEncoding.EncodeToString((mac.Sum(nil)))
|
||||
return fmt.Sprintf("UPYUN %s:%s", handler.Policy.AccessKey, signStr)
|
||||
}
|
||||
Reference in New Issue
Block a user