You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-library/vendor/github.com/qiniu/go-sdk/v7/storage/form_upload.go

406 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package storage
import (
"bytes"
"context"
"fmt"
"github.com/qiniu/go-sdk/v7/internal/hostprovider"
"hash"
"hash/crc32"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/client"
)
// PutExtra 为表单上传的额外可选项
type PutExtra struct {
// 可选,用户自定义参数,必须以 "x:" 开头。若不以x:开头,则忽略。
Params map[string]string
UpHost string
TryTimes int // 可选。尝试次数
// 主备域名冻结时间默认600s当一个域名请求失败单个域名会被重试 TryTimes 次),会被冻结一段时间,使用备用域名进行重试,在冻结时间内,域名不能被使用,当一个操作中所有域名竣备冻结操作不在进行重试,返回最后一次操作的错误。
HostFreezeDuration time.Duration
// 可选,当为 "" 时候,服务端自动判断。
MimeType string
// 上传事件:进度通知。这个事件的回调函数应该尽可能快地结束。
OnProgress func(fsize, uploaded int64)
}
func (extra *PutExtra) init() {
if extra.TryTimes == 0 {
extra.TryTimes = settings.TryTimes
}
if extra.HostFreezeDuration <= 0 {
extra.HostFreezeDuration = 10 * 60 * time.Second
}
}
func (extra *PutExtra) getUpHost(useHttps bool) string {
return hostAddSchemeIfNeeded(useHttps, extra.UpHost)
}
// PutRet 为七牛标准的上传回复内容。
// 如果使用了上传回调或者自定义了returnBody那么需要根据实际情况自己自定义一个返回值结构体
type PutRet struct {
Hash string `json:"hash"`
PersistentID string `json:"persistentId"`
Key string `json:"key"`
}
// FormUploader 表示一个表单上传的对象
type FormUploader struct {
Client *client.Client
Cfg *Config
}
// NewFormUploader 用来构建一个表单上传的对象
func NewFormUploader(cfg *Config) *FormUploader {
if cfg == nil {
cfg = &Config{}
}
return &FormUploader{
Client: &client.DefaultClient,
Cfg: cfg,
}
}
// NewFormUploaderEx 用来构建一个表单上传的对象
func NewFormUploaderEx(cfg *Config, clt *client.Client) *FormUploader {
if cfg == nil {
cfg = &Config{}
}
if clt == nil {
clt = &client.DefaultClient
}
return &FormUploader{
Client: clt,
Cfg: cfg,
}
}
// PutFile 用来以表单方式上传一个文件,和 Put 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.Reader 来访问。
//
// ctx 是请求的上下文。
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 callbackUrl 或 returnBody那么返回的数据结构是 PutRet 结构。
// uptoken 是由业务服务器颁发的上传凭证。
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外key 为空字符串是合法的。
// localFile 是要上传的文件的本地路径。
// extra 是上传的一些可选项可以指定为nil。详细见 PutExtra 结构的描述。
func (p *FormUploader) PutFile(
ctx context.Context, ret interface{}, uptoken, key, localFile string, extra *PutExtra) (err error) {
return p.putFile(ctx, ret, uptoken, key, true, localFile, extra)
}
// PutFileWithoutKey 用来以表单方式上传一个文件。不指定文件上传后保存的key的情况下文件命名方式首先看看
// uptoken 中是否设置了 saveKey如果设置了 saveKey那么按 saveKey 要求的规则生成 key否则自动以文件的 hash 做 key。
// 和 Put 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.Reader 来访问。
//
// ctx 是请求的上下文。
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody那么返回的数据结构是 PutRet 结构。
// uptoken 是由业务服务器颁发的上传凭证。
// localFile 是要上传的文件的本地路径。
// extra 是上传的一些可选项。可以指定为nil。详细见 PutExtra 结构的描述。
func (p *FormUploader) PutFileWithoutKey(
ctx context.Context, ret interface{}, uptoken, localFile string, extra *PutExtra) (err error) {
return p.putFile(ctx, ret, uptoken, "", false, localFile, extra)
}
func (p *FormUploader) putFile(
ctx context.Context, ret interface{}, upToken string,
key string, hasKey bool, localFile string, extra *PutExtra) (err error) {
f, err := os.Open(localFile)
if err != nil {
return
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return
}
fsize := fi.Size()
return p.put(ctx, ret, upToken, key, hasKey, f, fsize, extra, filepath.Base(localFile))
}
// Put 用来以表单方式上传一个文件。
//
// ctx 是请求的上下文。
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 callbackUrl 或 returnBody那么返回的数据结构是 PutRet 结构。
// uptoken 是由业务服务器颁发的上传凭证。
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外key 为空字符串是合法的。
// data 是文件内容的访问接口io.Reader
// fsize 是要上传的文件大小。
// extra 是上传的一些可选项。可以指定为nil。详细见 PutExtra 结构的描述。
func (p *FormUploader) Put(
ctx context.Context, ret interface{}, uptoken, key string, data io.Reader, size int64, extra *PutExtra) (err error) {
err = p.put(ctx, ret, uptoken, key, true, data, size, extra, path.Base(key))
return
}
// PutWithoutKey 用来以表单方式上传一个文件。不指定文件上传后保存的key的情况下文件命名方式首先看看 uptoken 中是否设置了 saveKey
// 如果设置了 saveKey那么按 saveKey 要求的规则生成 key否则自动以文件的 hash 做 key。
//
// ctx 是请求的上下文。
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody那么返回的数据结构是 PutRet 结构。
// uptoken 是由业务服务器颁发的上传凭证。
// data 是文件内容的访问接口io.Reader
// fsize 是要上传的文件大小。
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
func (p *FormUploader) PutWithoutKey(
ctx context.Context, ret interface{}, uptoken string, data io.Reader, size int64, extra *PutExtra) (err error) {
err = p.put(ctx, ret, uptoken, "", false, data, size, extra, "filename")
return err
}
func (p *FormUploader) put(
ctx context.Context, ret interface{}, upToken string,
key string, hasKey bool, data io.Reader, size int64, extra *PutExtra, fileName string) error {
if extra == nil {
extra = &PutExtra{}
}
extra.init()
seekableData, ok := data.(io.ReadSeeker)
if !ok {
dataBytes, rErr := ioutil.ReadAll(data)
if rErr != nil {
return rErr
}
if size <= 0 {
size = int64(len(dataBytes))
}
seekableData = bytes.NewReader(dataBytes)
}
return p.putSeekableData(ctx, ret, upToken, key, hasKey, seekableData, size, extra, fileName)
}
func (p *FormUploader) putSeekableData(ctx context.Context, ret interface{}, upToken string,
key string, hasKey bool, data io.ReadSeeker, dataSize int64, extra *PutExtra, fileName string) error {
formFieldBuff := new(bytes.Buffer)
formWriter := multipart.NewWriter(formFieldBuff)
// 写入表单头、token、key、fileName 等信息
if wErr := writeMultipart(formWriter, upToken, key, hasKey, extra, fileName); wErr != nil {
return wErr
}
// 计算文件 crc32
crc32Hash := crc32.NewIEEE()
if _, cErr := io.Copy(crc32Hash, data); cErr != nil {
return cErr
}
crcReader := newCrc32Reader(formWriter.Boundary(), crc32Hash)
crcBytes, rErr := ioutil.ReadAll(crcReader)
if rErr != nil {
return rErr
}
crcReader = nil
// 表单写入文件 crc32
if _, wErr := formFieldBuff.Write(crcBytes); wErr != nil {
return wErr
}
crcBytes = nil
formHead := make(textproto.MIMEHeader)
formHead.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`,
escapeQuotes(fileName)))
if extra.MimeType != "" {
formHead.Set("Content-Type", extra.MimeType)
}
if _, cErr := formWriter.CreatePart(formHead); cErr != nil {
return cErr
}
formHead = nil
// 表单 Fields
formFieldData := formFieldBuff.Bytes()
formFieldBuff = nil
// 表单最后一行
formEndLine := []byte(fmt.Sprintf("\r\n--%s--\r\n", formWriter.Boundary()))
// 不再重新构造 formBody ,避免内存峰值问题
var formBodyLen int64 = -1
if dataSize >= 0 {
formBodyLen = int64(len(formFieldData)) + dataSize + int64(len(formEndLine))
}
progress := newUploadProgress(extra.OnProgress)
getBodyReader := func() (io.Reader, error) {
if _, err := data.Seek(0, io.SeekStart); err != nil {
return nil, err
}
var formReader = io.MultiReader(bytes.NewReader(formFieldData), data, bytes.NewReader(formEndLine))
if extra.OnProgress != nil {
formReader = &readerWithProgress{reader: formReader, fsize: formBodyLen, onProgress: progress.onProgress}
}
return formReader, nil
}
getBodyReadCloser := func() (io.ReadCloser, error) {
reader, err := getBodyReader()
if err != nil {
return nil, err
}
return ioutil.NopCloser(reader), nil
}
var err error
var hostProvider hostprovider.HostProvider = nil
if extra.UpHost != "" {
hostProvider = hostprovider.NewWithHosts([]string{extra.getUpHost(p.Cfg.UseHTTPS)})
} else {
hostProvider, err = p.getUpHostProviderFromUploadToken(upToken, extra)
if err != nil {
return err
}
}
// 上传
contentType := formWriter.FormDataContentType()
headers := http.Header{}
headers.Add("Content-Type", contentType)
err = doUploadAction(hostProvider, extra.TryTimes, extra.HostFreezeDuration, func(host string) error {
reader, gErr := getBodyReader()
if gErr != nil {
return gErr
}
return p.Client.CallWithBodyGetter(ctx, ret, "POST", host, headers, reader, getBodyReadCloser, formBodyLen)
})
if err != nil {
return err
}
if extra.OnProgress != nil {
extra.OnProgress(formBodyLen, formBodyLen)
}
return nil
}
func (p *FormUploader) getUpHostProviderFromUploadToken(upToken string, extra *PutExtra) (hostprovider.HostProvider, error) {
ak, bucket, err := getAkBucketFromUploadToken(upToken)
if err != nil {
return nil, err
}
return getUpHostProvider(p.Cfg, extra.TryTimes, extra.HostFreezeDuration, ak, bucket)
}
type crc32Reader struct {
h hash.Hash32
boundary string
r io.Reader
inited bool
nlDashBoundaryNl string
header string
crc32PadLen int64
}
func newCrc32Reader(boundary string, h hash.Hash32) *crc32Reader {
nlDashBoundaryNl := fmt.Sprintf("\r\n--%s\r\n", boundary)
header := `Content-Disposition: form-data; name="crc32"` + "\r\n\r\n"
return &crc32Reader{
h: h,
boundary: boundary,
nlDashBoundaryNl: nlDashBoundaryNl,
header: header,
crc32PadLen: 10,
}
}
func (r *crc32Reader) Read(p []byte) (int, error) {
if !r.inited {
crc32Sum := r.h.Sum32()
crc32Line := r.nlDashBoundaryNl + r.header + fmt.Sprintf("%010d", crc32Sum) //padding crc32 results to 10 digits
r.r = strings.NewReader(crc32Line)
r.inited = true
}
return r.r.Read(p)
}
func (r crc32Reader) length() (length int64) {
return int64(len(r.nlDashBoundaryNl+r.header)) + r.crc32PadLen
}
func (p *FormUploader) UpHost(ak, bucket string) (upHost string, err error) {
return getUpHost(p.Cfg, 0, 0, ak, bucket)
}
type readerWithProgress struct {
reader io.Reader
uploaded int64
fsize int64
onProgress func(fsize, uploaded int64)
}
func (p *readerWithProgress) Read(b []byte) (n int, err error) {
if p.uploaded > 0 {
p.onProgress(p.fsize, p.uploaded)
}
n, err = p.reader.Read(b)
p.uploaded += int64(n)
if p.fsize > 0 && p.uploaded > p.fsize {
p.uploaded = p.fsize
}
return
}
func writeMultipart(writer *multipart.Writer, uptoken, key string, hasKey bool,
extra *PutExtra, fileName string) (err error) {
//token
if err = writer.WriteField("token", uptoken); err != nil {
return
}
//key
if hasKey {
if err = writer.WriteField("key", key); err != nil {
return
}
}
//extra.Params
if extra.Params != nil {
for k, v := range extra.Params {
if (strings.HasPrefix(k, "x:") || strings.HasPrefix(k, "x-qn-meta-")) && v != "" {
err = writer.WriteField(k, v)
if err != nil {
return
}
}
}
}
return err
}
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}