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

2 years ago
package storage
import (
"bytes"
"context"
"fmt"
"github.com/qiniu/go-sdk/v7/internal/hostprovider"
2 years ago
"hash"
"hash/crc32"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path"
"path/filepath"
"strings"
"time"
2 years ago
"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
2 years ago
// 可选,当为 "" 时候,服务端自动判断。
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
}
}
2 years ago
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,
2 years ago
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))
2 years ago
}
// 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)
}
2 years ago
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
2 years ago
}
// 计算文件 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
2 years ago
}
crcReader = nil
2 years ago
// 表单写入文件 crc32
if _, wErr := formFieldBuff.Write(crcBytes); wErr != nil {
return wErr
2 years ago
}
crcBytes = nil
2 years ago
formHead := make(textproto.MIMEHeader)
formHead.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`,
2 years ago
escapeQuotes(fileName)))
if extra.MimeType != "" {
formHead.Set("Content-Type", extra.MimeType)
2 years ago
}
if _, cErr := formWriter.CreatePart(formHead); cErr != nil {
return cErr
2 years ago
}
formHead = nil
2 years ago
// 表单 Fields
formFieldData := formFieldBuff.Bytes()
formFieldBuff = nil
2 years ago
// 表单最后一行
formEndLine := []byte(fmt.Sprintf("\r\n--%s--\r\n", formWriter.Boundary()))
2 years ago
// 不再重新构造 formBody ,避免内存峰值问题
var formBodyLen int64 = -1
if dataSize >= 0 {
formBodyLen = int64(len(formFieldData)) + dataSize + int64(len(formEndLine))
2 years ago
}
progress := newUploadProgress(extra.OnProgress)
2 years ago
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))
2 years ago
if extra.OnProgress != nil {
formReader = &readerWithProgress{reader: formReader, fsize: formBodyLen, onProgress: progress.onProgress}
2 years ago
}
return formReader, nil
}
getBodyReadCloser := func() (io.ReadCloser, error) {
reader, err := getBodyReader()
if err != nil {
return nil, err
}
return ioutil.NopCloser(reader), nil
}
11 months ago
var err error
var hostProvider hostprovider.HostProvider = nil
if extra.UpHost != "" {
hostProvider = hostprovider.NewWithHosts([]string{extra.getUpHost(p.Cfg.UseHTTPS)})
} else {
11 months ago
hostProvider, err = p.getUpHostProviderFromUploadToken(upToken, extra)
if err != nil {
return err
}
2 years ago
}
// 上传
contentType := formWriter.FormDataContentType()
2 years ago
headers := http.Header{}
headers.Add("Content-Type", contentType)
err = doUploadAction(hostProvider, extra.TryTimes, extra.HostFreezeDuration, func(host string) error {
11 months ago
reader, gErr := getBodyReader()
if gErr != nil {
return gErr
}
return p.Client.CallWithBodyGetter(ctx, ret, "POST", host, headers, reader, getBodyReadCloser, formBodyLen)
})
2 years ago
if err != nil {
return err
2 years ago
}
if extra.OnProgress != nil {
extra.OnProgress(formBodyLen, formBodyLen)
2 years ago
}
return nil
2 years ago
}
11 months ago
func (p *FormUploader) getUpHostProviderFromUploadToken(upToken string, extra *PutExtra) (hostprovider.HostProvider, error) {
ak, bucket, err := getAkBucketFromUploadToken(upToken)
if err != nil {
return nil, err
}
11 months ago
return getUpHostProvider(p.Cfg, extra.TryTimes, extra.HostFreezeDuration, ak, bucket)
}
2 years ago
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) {
11 months ago
return getUpHost(p.Cfg, 0, 0, ak, bucket)
2 years ago
}
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 {
2 years ago
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)
}