handler.go 9.4 KB
Newer Older
1 2 3 4
package oss

import (
	"context"
H
HFO4 已提交
5 6 7 8
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
9
	"errors"
H
HFO4 已提交
10
	"fmt"
11
	model "github.com/HFO4/cloudreve/models"
H
HFO4 已提交
12
	"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
13
	"github.com/HFO4/cloudreve/pkg/filesystem/response"
H
HFO4 已提交
14
	"github.com/HFO4/cloudreve/pkg/request"
15
	"github.com/HFO4/cloudreve/pkg/serializer"
H
HFO4 已提交
16
	"github.com/HFO4/cloudreve/pkg/util"
H
HFO4 已提交
17
	"github.com/aliyun/aliyun-oss-go-sdk/oss"
18 19
	"io"
	"net/url"
H
HFO4 已提交
20 21
	"path"
	"time"
22 23
)

H
HFO4 已提交
24 25 26 27 28 29 30 31 32 33 34 35 36
// 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"`
}

H
HFO4 已提交
37 38 39 40 41 42
// Driver 阿里云OSS策略适配器
type Driver struct {
	Policy     *model.Policy
	client     *oss.Client
	bucket     *oss.Bucket
	HTTPClient request.Client
H
HFO4 已提交
43 44
}

45
func (handler Driver) List(ctx context.Context, path string) ([]response.Object, error) {
46 47 48
	panic("implement me")
}

H
HFO4 已提交
49 50 51 52 53 54 55
type key int

const (
	// VersionID 文件版本标识
	VersionID key = iota
)

H
HFO4 已提交
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
// CORS 创建跨域策略
func (handler *Driver) CORS() error {
	// 初始化客户端
	if err := handler.InitOSSClient(); err != nil {
		return err
	}

	return handler.client.SetBucketCORS(handler.Policy.BucketName, []oss.CORSRule{
		{
			AllowedOrigin: []string{"*"},
			AllowedMethod: []string{
				"GET",
				"POST",
				"PUT",
				"DELETE",
				"HEAD",
			},
			ExposeHeader:  []string{},
			AllowedHeader: []string{"*"},
			MaxAgeSeconds: 3600,
		},
	})
}

H
HFO4 已提交
80
// InitOSSClient 初始化OSS鉴权客户端
H
HFO4 已提交
81
func (handler *Driver) InitOSSClient() error {
H
HFO4 已提交
82 83 84 85
	if handler.Policy == nil {
		return errors.New("存储策略为空")
	}

H
HFO4 已提交
86 87 88 89 90 91 92 93 94 95 96 97 98 99
	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
H
HFO4 已提交
100 101 102 103

	}

	return nil
104 105 106
}

// Get 获取文件
H
HFO4 已提交
107
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
H
HFO4 已提交
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
	// 通过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
	}

	// 获取文件数据流
H
HFO4 已提交
125
	resp, err := handler.HTTPClient.Request(
H
HFO4 已提交
126 127 128 129
		"GET",
		downloadURL,
		nil,
		request.WithContext(ctx),
130
		request.WithTimeout(time.Duration(0)),
H
HFO4 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143
	).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
144 145 146
}

// Put 将文件流保存到指定目录
H
HFO4 已提交
147
func (handler Driver) Put(ctx context.Context, file io.ReadCloser, dst string, size uint64) error {
H
HFO4 已提交
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
	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
169 170 171
}

// Delete 删除一个或多个文件,
H
HFO4 已提交
172
// 返回未删除的文件
H
HFO4 已提交
173
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
H
HFO4 已提交
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
	// 初始化客户端
	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
193 194 195
}

// Thumb 获取文件缩略图
H
HFO4 已提交
196
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
H
HFO4 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210
	// 初始化客户端
	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])
211
	ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, thumbParam)
H
HFO4 已提交
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
	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
227 228 229
}

// Source 获取外链URL
H
HFO4 已提交
230
func (handler Driver) Source(
231 232 233 234 235 236 237
	ctx context.Context,
	path string,
	baseURL url.URL,
	ttl int64,
	isDownload bool,
	speed int,
) (string, error) {
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
	// 初始化客户端
	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 {
255 256 257
		// Byte 转换为 bit
		speed *= 8

258 259 260 261 262 263 264 265 266 267 268 269 270
		// 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)
}

H
HFO4 已提交
271
func (handler Driver) signSourceURL(ctx context.Context, path string, ttl int64, options []oss.Option) (string, error) {
272 273 274 275 276 277 278 279 280 281
	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
	}
H
HFO4 已提交
282 283 284 285

	// 优先使用https
	finalURL.Scheme = "https"

286
	// 公有空间替换掉Key及不支持的头
H
HFO4 已提交
287 288 289 290
	if !handler.Policy.IsPrivate {
		query := finalURL.Query()
		query.Del("OSSAccessKeyId")
		query.Del("Signature")
291 292
		query.Del("response-content-disposition")
		query.Del("x-oss-traffic-limit")
H
HFO4 已提交
293 294 295 296 297 298 299 300 301 302 303
		finalURL.RawQuery = query.Encode()
	}

	if handler.Policy.BaseURL != "" {
		cdnURL, err := url.Parse(handler.Policy.BaseURL)
		if err != nil {
			return "", err
		}
		finalURL.Host = cdnURL.Host
		finalURL.Scheme = cdnURL.Scheme
	}
304 305

	return finalURL.String(), nil
306 307 308
}

// Token 获取上传策略和认证Token
H
HFO4 已提交
309
func (handler Driver) Token(ctx context.Context, TTL int64, key string) (serializer.UploadCredential, error) {
H
HFO4 已提交
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
	// 读取上下文中生成的存储路径
	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)},
		},
	}

H
HFO4 已提交
337 338 339 340 341
	if handler.Policy.MaxSize > 0 {
		postPolicy.Conditions = append(postPolicy.Conditions,
			[]interface{}{"content-length-range", 0, handler.Policy.MaxSize})
	}

H
HFO4 已提交
342 343 344
	return handler.getUploadCredential(ctx, postPolicy, callbackPolicy, TTL)
}

H
HFO4 已提交
345
func (handler Driver) getUploadCredential(ctx context.Context, policy UploadPolicy, callback CallbackPolicy, TTL int64) (serializer.UploadCredential, error) {
H
HFO4 已提交
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
	// 读取上下文中生成的存储路径
	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
384
}