From 1a1e551317c3d8fdd77e7540aa5dd4a35674ba48 Mon Sep 17 00:00:00 2001 From: "ze.zhao" Date: Mon, 6 Jan 2025 11:55:45 +0800 Subject: [PATCH] [feature] add GenPresignedUrl --- example/demo_presignedurl.go | 163 ++++++++++++++++++++++++++++ file.go | 23 ++++ file_mutipart_upload.go | 72 ++++++------ file_mutipart_upload_with_policy.go | 37 +++---- request.go | 1 + 5 files changed, 240 insertions(+), 56 deletions(-) create mode 100644 example/demo_presignedurl.go diff --git a/example/demo_presignedurl.go b/example/demo_presignedurl.go new file mode 100644 index 0000000..02e82ec --- /dev/null +++ b/example/demo_presignedurl.go @@ -0,0 +1,163 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + ufsdk "github.com/ufilesdk-dev/ufile-gosdk" + "github.com/ufilesdk-dev/ufile-gosdk/example/helper" +) + +func main() { + log.SetFlags(log.Lshortfile) + if _, err := os.Stat(helper.FakeBigFilePath); os.IsNotExist(err) { + helper.GenerateFakefile(helper.FakeBigFilePath, helper.FakeBigFileSize) + } + config, err := ufsdk.LoadConfig(helper.ConfigFile) + if err != nil { + panic(err.Error()) + } + u, err := ufsdk.NewFileRequest(config, nil) + if err != nil { + panic(err.Error()) + } + fileKey := helper.GenerateUniqKey() + err = putfile(helper.FakeBigFilePath, fileKey, u) + if err != nil { + log.Println("上传文件失败,具体错误详情:", err.Error()) + return + } + log.Println("上传文件成功") + + fileKey = helper.GenerateUniqKey() + err = mputfile(helper.FakeBigFilePath, fileKey, u) + if err != nil { + log.Println("分片上传文件失败,具体错误详情:", err.Error()) + return + } + log.Println("分片上传文件成功") +} + +func putfile(filePath string, keyName string, u *ufsdk.UFileRequest) error { + // 请确保在服务端生成该签名URL时设置的请求头与在使用URL时设置的请求头一致 + // u.RequestHeader = http.Header{} + // u.RequestHeader.Set("key", "value") + // 根据请求添加query + // u.RequestHeader = url.Values{} + // u.query.Set("key", "value") + + signedUrl := u.GenPresignedURL(keyName, 0, "PUT") + log.Println("上传文件的url 为:", signedUrl) + //使用url上传文件 + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + req, err := http.NewRequest("PUT", signedUrl, file) + if err != nil { + return err + } + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // 读取响应 + _, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + log.Println("返回上传状态码: ", resp.StatusCode) + if resp.StatusCode != 200 { + return fmt.Errorf("Remote response code is %d - %s not 2xx call DumpResponse(true) show details", + resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return nil +} + +// 分片上传 +func mputfile(filePath string, keyName string, u *ufsdk.UFileRequest) error { + // 创建一个新的HTTP客户端 + client := &http.Client{} + + //初始化分片上传 + state, err := u.InitiateMultipartUpload(keyName, "") + if err != nil { + return err + } + + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + chunk := make([]byte, state.BlkSize) + var partNumber int + for { + bytesRead, fileErr := file.Read(chunk) + if fileErr == io.EOF || bytesRead == 0 { //后面直接读到了结尾 + break + } + + // 获取分片上传的url + query := url.Values{} + query.Set("uploadId", state.UploadID) + query.Set("partNumber", strconv.Itoa(partNumber)) + u.RequestQuery = query + signedUrl := u.GenPresignedURL(keyName, 0, "PUT") + log.Println("上传分片的url 为:", signedUrl) + + // 使用url上传分片 + buf := bytes.NewBuffer(chunk[:bytesRead]) + req, err := http.NewRequest("PUT", signedUrl, buf) + if err != nil { + return err + } + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // 读取响应 + _, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + log.Println("返回上传状态码: ", resp.StatusCode) + if resp.StatusCode != 200 { + return fmt.Errorf("Remote response code is %d - %s not 2xx call DumpResponse(true) show details", + resp.StatusCode, http.StatusText(resp.StatusCode)) + } + if err != nil { + u.AbortMultipartUpload(state) + return err + } + // 解析出响应中的etag + etag := strings.Trim(resp.Header.Get("Etag"), "\"") //为保证线程安全,这里就不保留 lastResponse + if etag == "" { + etag = strings.Trim(resp.Header.Get("ETag"), "\"") //为保证线程安全,这里就不保留 lastResponse + } + state.Etags[partNumber] = etag + partNumber++ + } + + return u.FinishMultipartUpload(state) +} diff --git a/file.go b/file.go index 9ef0b48..89a4f65 100644 --- a/file.go +++ b/file.go @@ -377,6 +377,29 @@ func (u *UFileRequest) GetPrivateURL(keyName string, expiresDuation time.Duratio return reqURL + "?" + query.Encode() } +//GenPresignedURL 生成文件请求的签名URL。 +//keyName 表示请求 ufile 的文件名。 +//expiresDuation 表示链接的过期时间,从现在算起,24 * time.Hour 表示过期时间为一天。 +//method 表示http请求的method。 +//请求的query和header加入到UFileRequest中 +func (u *UFileRequest) GenPresignedURL(keyName string, expiresDuation time.Duration, method string) string { + t := time.Now() + t = t.Add(expiresDuation) + expires := strconv.FormatInt(t.Unix(), 10) + signature, publicKey := u.Auth.AuthorizationPrivateURL(method, u.BucketName, keyName, expires, u.RequestHeader) + query := url.Values{} + for k, v := range u.RequestQuery { + for i := 0; i < len(v); i++ { + query.Add(k, v[i]) + } + } + query.Set("UCloudPublicKey", publicKey) + query.Set("Signature", signature) + query.Set("Expires", expires) + reqURL := u.genFileURL(keyName) + return reqURL + "?" + query.Encode() +} + // Download 把文件下载到 HTTP Body 里面,这里只能用来下载小文件,建议使用 DownloadFile 来下载大文件。 func (u *UFileRequest) Download(reqURL string) error { req, err := http.NewRequest("GET", reqURL, nil) diff --git a/file_mutipart_upload.go b/file_mutipart_upload.go index 12a7463..679274b 100644 --- a/file_mutipart_upload.go +++ b/file_mutipart_upload.go @@ -14,17 +14,17 @@ import ( "sync" ) -//MultipartState 用于保存分片上传的中间状态 +// MultipartState 用于保存分片上传的中间状态 type MultipartState struct { BlkSize int //服务器返回的分片大小 - uploadID string + UploadID string mimeType string keyName string - etags map[int]string + Etags map[int]string mux sync.Mutex } -//UnmarshalJSON custom unmarshal json +// UnmarshalJSON custom unmarshal json func (m *MultipartState) UnmarshalJSON(bytes []byte) error { tmp := struct { BlkSize int `json:"BlkSize"` @@ -35,7 +35,7 @@ func (m *MultipartState) UnmarshalJSON(bytes []byte) error { return err } m.BlkSize = tmp.BlkSize - m.uploadID = tmp.UploadID + m.UploadID = tmp.UploadID return nil } @@ -63,10 +63,10 @@ type uploadChan struct { err error } -//MPut 分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是同步一片一片的上传。 -//mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 -//keyName 表示传到 ufile 的文件名。 -//大于 100M 的文件推荐使用本接口上传。 +// MPut 分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是同步一片一片的上传。 +// mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 +// keyName 表示传到 ufile 的文件名。 +// 大于 100M 的文件推荐使用本接口上传。 func (u *UFileRequest) MPut(filePath, keyName, mimeType string) error { file, err := openFile(filePath) if err != nil { @@ -101,16 +101,16 @@ func (u *UFileRequest) MPut(filePath, keyName, mimeType string) error { return u.FinishMultipartUpload(state) } -//AsyncMPut 异步分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是使用异步的方式同时传多个分片的块。 -//mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 -//keyName 表示传到 ufile 的文件名。 -//大于 100M 的文件推荐使用本接口上传。 -//同时并发上传的分片数量为10 +// AsyncMPut 异步分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是使用异步的方式同时传多个分片的块。 +// mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 +// keyName 表示传到 ufile 的文件名。 +// 大于 100M 的文件推荐使用本接口上传。 +// 同时并发上传的分片数量为10 func (u *UFileRequest) AsyncMPut(filePath, keyName, mimeType string) error { return u.AsyncUpload(filePath, keyName, mimeType, 10) } -//AsyncUpload AsyncMPut 的升级版, jobs 表示同时并发的数量。 +// AsyncUpload AsyncMPut 的升级版, jobs 表示同时并发的数量。 func (u *UFileRequest) AsyncUpload(filePath, keyName, mimeType string, jobs int) error { if jobs <= 0 { jobs = 1 @@ -181,11 +181,11 @@ func (u *UFileRequest) AsyncUpload(filePath, keyName, mimeType string, jobs int) return u.FinishMultipartUpload(state) } -//AbortMultipartUpload 取消分片上传,如果掉用 UploadPart 出现错误,可以调用本函数取消分片上传。 -//state 参数是 InitiateMultipartUpload 返回的 +// AbortMultipartUpload 取消分片上传,如果掉用 UploadPart 出现错误,可以调用本函数取消分片上传。 +// state 参数是 InitiateMultipartUpload 返回的 func (u *UFileRequest) AbortMultipartUpload(state *MultipartState) error { query := &url.Values{} - query.Add("uploadId", state.uploadID) + query.Add("uploadId", state.UploadID) reqURL := u.genFileURL(state.keyName) + "?" + query.Encode() req, err := http.NewRequest("DELETE", reqURL, nil) @@ -197,11 +197,11 @@ func (u *UFileRequest) AbortMultipartUpload(state *MultipartState) error { return u.request(req) } -//InitiateMultipartUpload 初始化分片上传,返回一个 state 用于后续的 UploadPart, FinishMultipartUpload, AbortMultipartUpload 的接口。 +// InitiateMultipartUpload 初始化分片上传,返回一个 state 用于后续的 UploadPart, FinishMultipartUpload, AbortMultipartUpload 的接口。 // -//keyName 表示传到 ufile 的文件名。 +// keyName 表示传到 ufile 的文件名。 // -//mimeType 表示文件的 mimeType, 传空会报错,你可以使用 GetFileMimeType 方法检测文件的 mimeType。如果您上传的不是文件,您可以使用 http.DetectContentType https://golang.org/src/net/http/sniff.go?s=646:688#L11进行检测。 +// mimeType 表示文件的 mimeType, 传空会报错,你可以使用 GetFileMimeType 方法检测文件的 mimeType。如果您上传的不是文件,您可以使用 http.DetectContentType https://golang.org/src/net/http/sniff.go?s=646:688#L11进行检测。 func (u *UFileRequest) InitiateMultipartUpload(keyName, mimeType string) (*MultipartState, error) { reqURL := u.genFileURL(keyName) + "?uploads" req, err := http.NewRequest("POST", reqURL, nil) @@ -231,18 +231,18 @@ func (u *UFileRequest) InitiateMultipartUpload(keyName, mimeType string) (*Multi return nil, err } response.keyName = keyName - response.etags = make(map[int]string) + response.Etags = make(map[int]string) response.mimeType = mimeType return response, err } -//UploadPart 上传一个分片,buf 就是分片数据,buf 的数据块大小必须为 state.BlkSize,否则会报错。 -//pardNumber 表示第几个分片,从 0 开始。例如一个文件按 state.BlkSize 分为 5 块,那么分片分别是 0,1,2,3,4。 -//state 参数是 InitiateMultipartUpload 返回的 +// UploadPart 上传一个分片,buf 就是分片数据,buf 的数据块大小必须为 state.BlkSize,否则会报错。 +// pardNumber 表示第几个分片,从 0 开始。例如一个文件按 state.BlkSize 分为 5 块,那么分片分别是 0,1,2,3,4。 +// state 参数是 InitiateMultipartUpload 返回的 func (u *UFileRequest) UploadPart(buf *bytes.Buffer, state *MultipartState, partNumber int) error { query := &url.Values{} - query.Add("uploadId", state.uploadID) + query.Add("uploadId", state.UploadID) query.Add("partNumber", strconv.Itoa(partNumber)) reqURL := u.genFileURL(state.keyName) + "?" + query.Encode() @@ -280,15 +280,15 @@ func (u *UFileRequest) UploadPart(buf *bytes.Buffer, state *MultipartState, part etag = strings.Trim(resp.Header.Get("ETag"), "\"") //为保证线程安全,这里就不保留 lastResponse } state.mux.Lock() - state.etags[partNumber] = etag + state.Etags[partNumber] = etag state.mux.Unlock() return nil } -//UploadPartCopy 实现从一个已存在的Object中拷贝数据来上传一个Part。 +// UploadPartCopy 实现从一个已存在的Object中拷贝数据来上传一个Part。 func (u *UFileRequest) UploadPartCopy(state *MultipartState, partNumber int, sourceBucketName, sourceObject string, offset, size int64) error { query := &url.Values{} - query.Add("uploadId", state.uploadID) + query.Add("uploadId", state.UploadID) query.Add("partNumber", strconv.Itoa(partNumber)) reqURL := u.genFileURL(state.keyName) + "?" + query.Encode() @@ -322,21 +322,21 @@ func (u *UFileRequest) UploadPartCopy(state *MultipartState, partNumber int, sou etag = strings.Trim(resp.Header.Get("ETag"), "\"") //为保证线程安全,这里就不保留 lastResponse } state.mux.Lock() - state.etags[partNumber] = etag + state.Etags[partNumber] = etag state.mux.Unlock() return nil } -//FinishMultipartUpload 完成分片上传。分片上传必须要调用的接口。 -//state 参数是 InitiateMultipartUpload 返回的 +// FinishMultipartUpload 完成分片上传。分片上传必须要调用的接口。 +// state 参数是 InitiateMultipartUpload 返回的 func (u *UFileRequest) FinishMultipartUpload(state *MultipartState) error { query := &url.Values{} - query.Add("uploadId", state.uploadID) + query.Add("uploadId", state.UploadID) reqURL := u.genFileURL(state.keyName) + "?" + query.Encode() var etagsStr string - etagLen := len(state.etags) + etagLen := len(state.Etags) for i := 0; i != etagLen; i++ { - etagsStr += state.etags[i] + etagsStr += state.Etags[i] if i != etagLen-1 { etagsStr += "," } @@ -360,7 +360,7 @@ func divideCeil(a, b int64) int { return int(c) } -//ListParts 获取已上传成功的分片列表 +// ListParts 获取已上传成功的分片列表 func (u *UFileRequest) ListParts(uploadId string, maxParts, partNumberMarker int) (list ListPartsResponse, err error) { if maxParts == 0 { maxParts = 100 diff --git a/file_mutipart_upload_with_policy.go b/file_mutipart_upload_with_policy.go index 51d29fa..c55a541 100644 --- a/file_mutipart_upload_with_policy.go +++ b/file_mutipart_upload_with_policy.go @@ -2,8 +2,8 @@ package ufsdk import ( "bytes" + "encoding/base64" "io" - "encoding/base64" "net/http" "net/url" "strconv" @@ -13,10 +13,10 @@ import ( //带回调策略的mput 接口,一些基础函数依赖于 file_mput 定义的函数 -//MPut 分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是同步一片一片的上传。 -//mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 -//keyName 表示传到 ufile 的文件名。 -//大于 100M 的文件推荐使用本接口上传。 +// MPut 分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是同步一片一片的上传。 +// mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 +// keyName 表示传到 ufile 的文件名。 +// 大于 100M 的文件推荐使用本接口上传。 func (u *UFileRequest) MPutWithPolicy(filePath, keyName, mimeType string, policy_json string) error { file, err := openFile(filePath) if err != nil { @@ -51,16 +51,16 @@ func (u *UFileRequest) MPutWithPolicy(filePath, keyName, mimeType string, policy return u.FinishMultipartUploadWithPolicy(state, policy_json) } -//AsyncMPut 异步分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是使用异步的方式同时传多个分片的块。 -//mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 -//keyName 表示传到 ufile 的文件名。 -//大于 100M 的文件推荐使用本接口上传。 -//同时并发上传的分片数量为10 +// AsyncMPut 异步分片上传一个文件,filePath 是本地文件所在的路径,内部会自动对文件进行分片上传,上传的方式是使用异步的方式同时传多个分片的块。 +// mimeType 如果为空的话,会调用 net/http 里面的 DetectContentType 进行检测。 +// keyName 表示传到 ufile 的文件名。 +// 大于 100M 的文件推荐使用本接口上传。 +// 同时并发上传的分片数量为10 func (u *UFileRequest) AsyncMPutWithPolicy(filePath, keyName, mimeType string, policy_json string) error { return u.AsyncUploadWithPolicy(filePath, keyName, mimeType, 10, policy_json) } -//AsyncUpload AsyncMPut 的升级版, jobs 表示同时并发的数量。 +// AsyncUpload AsyncMPut 的升级版, jobs 表示同时并发的数量。 func (u *UFileRequest) AsyncUploadWithPolicy(filePath, keyName, mimeType string, jobs int, policy_json string) error { if jobs <= 0 { jobs = 1 @@ -131,17 +131,16 @@ func (u *UFileRequest) AsyncUploadWithPolicy(filePath, keyName, mimeType string, return u.FinishMultipartUploadWithPolicy(state, policy_json) } - -//FinishMultipartUpload 完成分片上传。分片上传必须要调用的接口。 -//state 参数是 InitiateMultipartUpload 返回的 +// FinishMultipartUpload 完成分片上传。分片上传必须要调用的接口。 +// state 参数是 InitiateMultipartUpload 返回的 func (u *UFileRequest) FinishMultipartUploadWithPolicy(state *MultipartState, policy_json string) error { query := &url.Values{} - query.Add("uploadId", state.uploadID) + query.Add("uploadId", state.UploadID) reqURL := u.genFileURL(state.keyName) + "?" + query.Encode() var etagsStr string - etagLen := len(state.etags) + etagLen := len(state.Etags) for i := 0; i != etagLen; i++ { - etagsStr += state.etags[i] + etagsStr += state.Etags[i] if i != etagLen-1 { etagsStr += "," } @@ -151,7 +150,7 @@ func (u *UFileRequest) FinishMultipartUploadWithPolicy(state *MultipartState, po if err != nil { return err } - + req.Header.Add("Content-Type", state.mimeType) policy := base64.URLEncoding.EncodeToString([]byte(policy_json)) @@ -162,5 +161,3 @@ func (u *UFileRequest) FinishMultipartUploadWithPolicy(state *MultipartState, po return u.request(req) } - - diff --git a/request.go b/request.go index b252e4a..efd682e 100644 --- a/request.go +++ b/request.go @@ -28,6 +28,7 @@ type UFileRequest struct { Context context.Context baseURL *url.URL RequestHeader http.Header + RequestQuery url.Values LastResponseStatus int LastResponseHeader http.Header