鸣镝 8 лет назад
Родитель
Сommit
b5ba907a21
13 измененных файлов с 918 добавлено и 100 удалено
  1. 10 5
      oss/auth.go
  2. 276 23
      oss/bucket.go
  3. 189 0
      oss/bucket_test.go
  4. 63 26
      oss/client.go
  5. 33 0
      oss/client_test.go
  6. 169 1
      oss/conn.go
  7. 31 3
      oss/const.go
  8. 2 2
      oss/download.go
  9. 1 1
      oss/download_test.go
  10. 23 13
      oss/multipart.go
  11. 41 23
      oss/option.go
  12. 11 3
      oss/option_test.go
  13. 69 0
      oss/progress_test.go

+ 10 - 5
oss/auth.go

@@ -20,6 +20,14 @@ type headerSorter struct {
 
 // 生成签名方法(直接设置请求的Header)。
 func (conn Conn) signHeader(req *http.Request, canonicalizedResource string) {
+	// Get the final Authorization' string
+	authorizationStr := "OSS " + conn.config.AccessKeyID + ":" + conn.getSignedStr(req, canonicalizedResource)
+
+	// Give the parameter "Authorization" value
+	req.Header.Set(HTTPHeaderAuthorization, authorizationStr)
+}
+
+func (conn Conn) getSignedStr(req *http.Request, canonicalizedResource string) string {
 	// Find out the "x-oss-"'s address in this request'header
 	temp := make(map[string]string)
 
@@ -40,6 +48,7 @@ func (conn Conn) signHeader(req *http.Request, canonicalizedResource string) {
 	}
 
 	// Give other parameters values
+	// when sign url, date is expires
 	date := req.Header.Get(HTTPHeaderDate)
 	contentType := req.Header.Get(HTTPHeaderContentType)
 	contentMd5 := req.Header.Get(HTTPHeaderContentMD5)
@@ -49,11 +58,7 @@ func (conn Conn) signHeader(req *http.Request, canonicalizedResource string) {
 	io.WriteString(h, signStr)
 	signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil))
 
-	// Get the final Authorization' string
-	authorizationStr := "OSS " + conn.config.AccessKeyID + ":" + signedStr
-
-	// Give the parameter "Authorization" value
-	req.Header.Set(HTTPHeaderAuthorization, authorizationStr)
+	return signedStr
 }
 
 // Additional function for function SignHeader.

+ 276 - 23
oss/bucket.go

@@ -5,6 +5,7 @@ import (
 	"crypto/md5"
 	"encoding/base64"
 	"encoding/xml"
+	"fmt"
 	"hash"
 	"hash/crc64"
 	"io"
@@ -13,6 +14,7 @@ import (
 	"net/url"
 	"os"
 	"strconv"
+	"time"
 )
 
 // Bucket implements the operations of object.
@@ -96,7 +98,8 @@ func (bucket Bucket) DoPutObject(request *PutObjectRequest, options []Option) (*
 
 	listener := getProgressListener(options)
 
-	resp, err := bucket.do("PUT", request.ObjectKey, "", "", options, request.Reader, listener)
+	params := map[string]interface{}{}
+	resp, err := bucket.do("PUT", request.ObjectKey, params, options, request.Reader, listener)
 	if err != nil {
 		return nil, err
 	}
@@ -188,7 +191,8 @@ func (bucket Bucket) GetObjectToFile(objectKey, filePath string, options ...Opti
 // error  操作无错误为nil,非nil为错误信息。
 //
 func (bucket Bucket) DoGetObject(request *GetObjectRequest, options []Option) (*GetObjectResult, error) {
-	resp, err := bucket.do("GET", request.ObjectKey, "", "", options, nil, nil)
+	params := map[string]interface{}{}
+	resp, err := bucket.do("GET", request.ObjectKey, params, options, nil, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -231,7 +235,8 @@ func (bucket Bucket) DoGetObject(request *GetObjectRequest, options []Option) (*
 func (bucket Bucket) CopyObject(srcObjectKey, destObjectKey string, options ...Option) (CopyObjectResult, error) {
 	var out CopyObjectResult
 	options = append(options, CopySource(bucket.BucketName, url.QueryEscape(srcObjectKey)))
-	resp, err := bucket.do("PUT", destObjectKey, "", "", options, nil, nil)
+	params := map[string]interface{}{}
+	resp, err := bucket.do("PUT", destObjectKey, params, options, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -284,7 +289,8 @@ func (bucket Bucket) copy(srcObjectKey, destBucketName, destObjectKey string, op
 	if err != nil {
 		return out, err
 	}
-	resp, err := bucket.Client.Conn.Do("PUT", destBucketName, destObjectKey, "", "", headers, nil, 0, nil)
+	params := map[string]interface{}{}
+	resp, err := bucket.Client.Conn.Do("PUT", destBucketName, destObjectKey, params, headers, nil, 0, nil)
 	if err != nil {
 		return out, err
 	}
@@ -319,6 +325,9 @@ func (bucket Bucket) AppendObject(objectKey string, reader io.Reader, appendPosi
 	}
 
 	result, err := bucket.DoAppendObject(request, options)
+	if err != nil {
+		return appendPosition, err
+	}
 
 	return result.NextPosition, err
 }
@@ -333,7 +342,9 @@ func (bucket Bucket) AppendObject(objectKey string, reader io.Reader, appendPosi
 // error  操作无错误为nil,非nil为错误信息。
 //
 func (bucket Bucket) DoAppendObject(request *AppendObjectRequest, options []Option) (*AppendObjectResult, error) {
-	params := "append&position=" + strconv.FormatInt(request.Position, 10)
+	params := map[string]interface{}{}
+	params["append"] = nil
+	params["position"] = strconv.FormatInt(request.Position, 10)
 	headers := make(map[string]string)
 
 	opts := addContentType(options, request.ObjectKey)
@@ -348,7 +359,7 @@ func (bucket Bucket) DoAppendObject(request *AppendObjectRequest, options []Opti
 	listener := getProgressListener(options)
 
 	handleOptions(headers, opts)
-	resp, err := bucket.Client.Conn.Do("POST", bucket.BucketName, request.ObjectKey, params, params, headers,
+	resp, err := bucket.Client.Conn.Do("POST", bucket.BucketName, request.ObjectKey, params, headers,
 		request.Reader, initCRC, listener)
 	if err != nil {
 		return nil, err
@@ -379,7 +390,8 @@ func (bucket Bucket) DoAppendObject(request *AppendObjectRequest, options []Opti
 // error 操作无错误为nil,非nil为错误信息。
 //
 func (bucket Bucket) DeleteObject(objectKey string) error {
-	resp, err := bucket.do("DELETE", objectKey, "", "", nil, nil, nil)
+	params := map[string]interface{}{}
+	resp, err := bucket.do("DELETE", objectKey, params, nil, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -404,7 +416,6 @@ func (bucket Bucket) DeleteObjects(objectKeys []string, options ...Option) (Dele
 	}
 	isQuiet, _ := findOption(options, deleteObjectsQuiet, false)
 	dxml.Quiet = isQuiet.(bool)
-	encode := "&encoding-type=url"
 
 	bs, err := xml.Marshal(dxml)
 	if err != nil {
@@ -418,7 +429,12 @@ func (bucket Bucket) DeleteObjects(objectKeys []string, options ...Option) (Dele
 	sum := md5.Sum(bs)
 	b64 := base64.StdEncoding.EncodeToString(sum[:])
 	options = append(options, ContentMD5(b64))
-	resp, err := bucket.do("POST", "", "delete"+encode, "delete", options, buffer, nil)
+
+	params := map[string]interface{}{}
+	params["delete"] = nil
+	params["encoding-type"] = "url"
+
+	resp, err := bucket.do("POST", "", params, options, buffer, nil)
 	if err != nil {
 		return out, err
 	}
@@ -478,12 +494,12 @@ func (bucket Bucket) ListObjects(options ...Option) (ListObjectsResult, error) {
 	var out ListObjectsResult
 
 	options = append(options, EncodingType("url"))
-	params, err := handleParams(options)
+	params, err := getRawParams(options)
 	if err != nil {
 		return out, err
 	}
 
-	resp, err := bucket.do("GET", "", params, "", nil, nil, nil)
+	resp, err := bucket.do("GET", "", params, nil, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -524,7 +540,8 @@ func (bucket Bucket) SetObjectMeta(objectKey string, options ...Option) error {
 // error  操作无错误为nil,非nil为错误信息。
 //
 func (bucket Bucket) GetObjectDetailedMeta(objectKey string, options ...Option) (http.Header, error) {
-	resp, err := bucket.do("HEAD", objectKey, "", "", options, nil, nil)
+	params := map[string]interface{}{}
+	resp, err := bucket.do("HEAD", objectKey, params, options, nil, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -545,7 +562,10 @@ func (bucket Bucket) GetObjectDetailedMeta(objectKey string, options ...Option)
 // error 操作无错误为nil,非nil为错误信息。
 //
 func (bucket Bucket) GetObjectMeta(objectKey string) (http.Header, error) {
-	resp, err := bucket.do("GET", objectKey, "?objectMeta", "", nil, nil, nil)
+	params := map[string]interface{}{}
+	params["objectMeta"] = nil
+	//resp, err := bucket.do("GET", objectKey, "?objectMeta", "", nil, nil, nil)
+	resp, err := bucket.do("GET", objectKey, params, nil, nil, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -573,7 +593,9 @@ func (bucket Bucket) GetObjectMeta(objectKey string) (http.Header, error) {
 //
 func (bucket Bucket) SetObjectACL(objectKey string, objectACL ACLType) error {
 	options := []Option{ObjectACL(objectACL)}
-	resp, err := bucket.do("PUT", objectKey, "acl", "acl", options, nil, nil)
+	params := map[string]interface{}{}
+	params["acl"] = nil
+	resp, err := bucket.do("PUT", objectKey, params, options, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -591,7 +613,9 @@ func (bucket Bucket) SetObjectACL(objectKey string, objectACL ACLType) error {
 //
 func (bucket Bucket) GetObjectACL(objectKey string) (GetObjectACLResult, error) {
 	var out GetObjectACLResult
-	resp, err := bucket.do("GET", objectKey, "acl", "acl", nil, nil, nil)
+	params := map[string]interface{}{}
+	params["acl"] = nil
+	resp, err := bucket.do("GET", objectKey, params, nil, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -617,7 +641,9 @@ func (bucket Bucket) GetObjectACL(objectKey string) (GetObjectACLResult, error)
 //
 func (bucket Bucket) PutSymlink(symObjectKey string, targetObjectKey string, options ...Option) error {
 	options = append(options, symlinkTarget(url.QueryEscape(targetObjectKey)))
-	resp, err := bucket.do("PUT", symObjectKey, "symlink", "symlink", options, nil, nil)
+	params := map[string]interface{}{}
+	params["symlink"] = nil
+	resp, err := bucket.do("PUT", symObjectKey, params, options, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -634,7 +660,9 @@ func (bucket Bucket) PutSymlink(symObjectKey string, targetObjectKey string, opt
 // error 操作无错误为nil,非nil为错误信息。当error为nil时,返回的string为目标文件,否则该值无效。
 //
 func (bucket Bucket) GetSymlink(objectKey string) (http.Header, error) {
-	resp, err := bucket.do("GET", objectKey, "symlink", "symlink", nil, nil, nil)
+	params := map[string]interface{}{}
+	params["symlink"] = nil
+	resp, err := bucket.do("GET", objectKey, params, nil, nil, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -652,16 +680,20 @@ func (bucket Bucket) GetSymlink(objectKey string) (http.Header, error) {
 //
 // RestoreObject 恢复处于冷冻状态的归档类型Object进入读就绪状态。
 //
-// 如果是针对该Object第一次调用restore接口,则返回成功。
-// 如果已经成功调用过restore接口,且restore没有完全完成,再次调用时返回409,错误码:RestoreAlreadyInProgress。
-// 如果已经成功调用过restore接口,且restore已经完成,再次调用时返回成功,且会将object的可下载时间延长一天,最多延长7天。
+// 一个Archive类型的object初始时处于冷冻状态。
+//
+// 针对处于冷冻状态的object调用restore命令,返回成功。object处于解冻中,服务端执行解冻,在此期间再次调用restore命令,同样成功,且不会延长object可读状态持续时间。
+// 待服务端执行完成解冻任务后,object就进入了解冻状态,此时用户可以读取object。
+// 解冻状态默认持续1天,对于解冻状态的object调用restore命令,会将object的解冻状态延长一天,最多可以延长到7天,之后object又回到初始时的冷冻状态。
 //
 // objectKey 需要恢复状态的object名称。
 //
 // error 操作无错误为nil,非nil为错误信息。
 //
 func (bucket Bucket) RestoreObject(objectKey string) error {
-	resp, err := bucket.do("POST", objectKey, "restore", "restore", nil, nil, nil)
+	params := map[string]interface{}{}
+	params["restore"] = nil
+	resp, err := bucket.do("POST", objectKey, params, nil, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -669,8 +701,219 @@ func (bucket Bucket) RestoreObject(objectKey string) error {
 	return checkRespCode(resp.StatusCode, []int{http.StatusOK, http.StatusAccepted})
 }
 
+//
+// SignURL 获取签名URL。
+//
+// objectKey 获取URL的object。
+// signURLConfig 获取URL的配置。
+//
+// 返回URL字符串,error为nil时有效。
+// error 操作无错误为nil,非nil为错误信息。
+//
+func (bucket Bucket) SignURL(objectKey string, method HTTPMethod, expiredInSec int64, options ...Option) (string, error) {
+	if expiredInSec < 0 {
+		return "", fmt.Errorf("invalid expires: %d, expires must bigger than 0", expiredInSec)
+	}
+	expiration := time.Now().Unix() + expiredInSec
+
+	params, err := getRawParams(options)
+	if err != nil {
+		return "", err
+	}
+
+	headers := make(map[string]string)
+	err = handleOptions(headers, options)
+	if err != nil {
+		return "", err
+	}
+
+	return bucket.Client.Conn.signURL(method, bucket.BucketName, objectKey, expiration, params, headers), nil
+}
+
+//
+// PutObjectWithURL 新建Object,如果Object已存在,覆盖原有Object。
+// PutObjectWithURL 不会根据key生成minetype。
+//
+// signedURL  签名的URL。
+// reader     io.Reader读取object的数据。
+// options    上传对象时可以指定对象的属性,可用选项有CacheControl、ContentDisposition、ContentEncoding、
+// Expires、ServerSideEncryption、ObjectACL、Meta,具体含义请参看
+// https://help.aliyun.com/document_detail/oss/api-reference/object/PutObject.html
+//
+// error  操作无错误为nil,非nil为错误信息。
+//
+func (bucket Bucket) PutObjectWithURL(signedURL string, reader io.Reader, options ...Option) error {
+	resp, err := bucket.DoPutObjectWithURL(signedURL, reader, options)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	return err
+}
+
+//
+// PutObjectFromFileWithURL 新建Object,内容从本地文件中读取。
+// PutObjectFromFileWithURL 不会根据key、filePath生成minetype。
+//
+// signedURL  签名的URL。
+// filePath  本地文件,如 dir/file.txt,上传对象的值为该文件内容。
+// options   上传对象时可以指定对象的属性。详见PutObject的options。
+//
+// error  操作无错误为nil,非nil为错误信息。
+//
+func (bucket Bucket) PutObjectFromFileWithURL(signedURL, filePath string, options ...Option) error {
+	fd, err := os.Open(filePath)
+	if err != nil {
+		return err
+	}
+	defer fd.Close()
+
+	resp, err := bucket.DoPutObjectWithURL(signedURL, fd, options)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	return err
+}
+
+//
+// DoPutObjectWithURL 上传文件。
+//
+// signedURL  签名的URL。
+// reader     io.Reader读取object的数据。
+// options  上传选项。
+//
+// Response 上传请求返回值。
+// error  操作无错误为nil,非nil为错误信息。
+//
+func (bucket Bucket) DoPutObjectWithURL(signedURL string, reader io.Reader, options []Option) (*Response, error) {
+	listener := getProgressListener(options)
+
+	params := map[string]interface{}{}
+	resp, err := bucket.doURL("PUT", signedURL, params, options, reader, listener)
+	if err != nil {
+		return nil, err
+	}
+
+	if bucket.getConfig().IsEnableCRC {
+		err = checkCRC(resp, "DoPutObjectWithURL")
+		if err != nil {
+			return resp, err
+		}
+	}
+
+	err = checkRespCode(resp.StatusCode, []int{http.StatusOK})
+
+	return resp, err
+}
+
+//
+// GetObjectWithURL 下载文件。
+//
+// signedURL  签名的URL。
+// options   对象的属性限制项,可选值有Range、IfModifiedSince、IfUnmodifiedSince、IfMatch、
+// IfNoneMatch、AcceptEncoding,详细请参考
+// https://help.aliyun.com/document_detail/oss/api-reference/object/GetObject.html
+//
+// io.ReadCloser  reader,读取数据后需要close。error为nil时有效。
+// error  操作无错误为nil,非nil为错误信息。
+//
+func (bucket Bucket) GetObjectWithURL(signedURL string, options ...Option) (io.ReadCloser, error) {
+	result, err := bucket.DoGetObjectWithURL(signedURL, options)
+	if err != nil {
+		return nil, err
+	}
+	return result.Response.Body, nil
+}
+
+//
+// GetObjectToFile 下载文件。
+//
+// signedURL  签名的URL。
+// filePath   下载对象的内容写到该本地文件。
+// options    对象的属性限制项。详见GetObject的options。
+//
+// error  操作无错误时返回error为nil,非nil为错误说明。
+//
+func (bucket Bucket) GetObjectToFileWithURL(signedURL, filePath string, options ...Option) error {
+	tempFilePath := filePath + TempFileSuffix
+
+	// 读取Object内容
+	result, err := bucket.DoGetObjectWithURL(signedURL, options)
+	if err != nil {
+		return err
+	}
+	defer result.Response.Body.Close()
+
+	// 如果文件不存在则创建,存在则清空
+	fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, FilePermMode)
+	if err != nil {
+		return err
+	}
+
+	// 存储数据到文件
+	_, err = io.Copy(fd, result.Response.Body)
+	fd.Close()
+	if err != nil {
+		return err
+	}
+
+	// 比较CRC值
+	hasRange, _, _ := isOptionSet(options, HTTPHeaderRange)
+	if bucket.getConfig().IsEnableCRC && !hasRange {
+		result.Response.ClientCRC = result.ClientCRC.Sum64()
+		err = checkCRC(result.Response, "GetObjectToFileWithURL")
+		if err != nil {
+			os.Remove(tempFilePath)
+			return err
+		}
+	}
+
+	return os.Rename(tempFilePath, filePath)
+}
+
+//
+// DoGetObjectWithURL 下载文件
+//
+// signedURL  签名的URL。
+// options    对象的属性限制项。详见GetObject的options。
+//
+// GetObjectResult 下载请求返回值。
+// error  操作无错误为nil,非nil为错误信息。
+//
+func (bucket Bucket) DoGetObjectWithURL(signedURL string, options []Option) (*GetObjectResult, error) {
+	params := map[string]interface{}{}
+	resp, err := bucket.doURL("GET", signedURL, params, options, nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	result := &GetObjectResult{
+		Response: resp,
+	}
+
+	// crc
+	var crcCalc hash.Hash64
+	hasRange, _, _ := isOptionSet(options, HTTPHeaderRange)
+	if bucket.getConfig().IsEnableCRC && !hasRange {
+		crcCalc = crc64.New(crcTable())
+		result.ServerCRC = resp.ServerCRC
+		result.ClientCRC = crcCalc
+	}
+
+	// progress
+	listener := getProgressListener(options)
+
+	contentLen, _ := strconv.ParseInt(resp.Headers.Get(HTTPHeaderContentLength), 10, 64)
+	resp.Body = ioutil.NopCloser(TeeReader(resp.Body, crcCalc, contentLen, listener, nil))
+
+	return result, nil
+}
+
 // Private
-func (bucket Bucket) do(method, objectName, urlParams, subResource string, options []Option,
+func (bucket Bucket) do(method, objectName string, params map[string]interface{}, options []Option,
 	data io.Reader, listener ProgressListener) (*Response, error) {
 	headers := make(map[string]string)
 	err := handleOptions(headers, options)
@@ -678,7 +921,17 @@ func (bucket Bucket) do(method, objectName, urlParams, subResource string, optio
 		return nil, err
 	}
 	return bucket.Client.Conn.Do(method, bucket.BucketName, objectName,
-		urlParams, subResource, headers, data, 0, listener)
+		params, headers, data, 0, listener)
+}
+
+func (bucket Bucket) doURL(method HTTPMethod, signedURL string, params map[string]interface{}, options []Option,
+	data io.Reader, listener ProgressListener) (*Response, error) {
+	headers := make(map[string]string)
+	err := handleOptions(headers, options)
+	if err != nil {
+		return nil, err
+	}
+	return bucket.Client.Conn.DoURL(method, signedURL, headers, data, 0, listener)
 }
 
 func (bucket Bucket) getConfig() *Config {

+ 189 - 0
oss/bucket_test.go

@@ -197,6 +197,180 @@ func (s *OssBucketSuite) TestPutObject(c *C) {
 	c.Assert(err, IsNil)
 }
 
+func (s *OssBucketSuite) TestSignURL(c *C) {
+	objectName := objectNamePrefix + randStr(5)
+	objectValue := randStr(20)
+
+	filePath := randLowStr(10)
+	content := "复写object"
+	createFile(filePath, content, c)
+
+	notExistfilePath := randLowStr(10)
+	os.Remove(notExistfilePath)
+
+	// sign url for put
+	str, err := s.bucket.SignURL(objectName, HTTPPut, 60)
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// error put object with url
+	err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue), ContentType("image/tiff"))
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+
+	err = s.bucket.PutObjectFromFileWithURL(str, filePath, ContentType("image/tiff"))
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+
+	// put object with url
+	err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue))
+	c.Assert(err, IsNil)
+
+	acl, err := s.bucket.GetObjectACL(objectName)
+	c.Assert(err, IsNil)
+	c.Assert(acl.ACL, Equals, "default")
+
+	// get object meta
+	meta, err := s.bucket.GetObjectDetailedMeta(objectName)
+	c.Assert(err, IsNil)
+	c.Assert(meta.Get(HTTPHeaderContentType), Equals, "application/octet-stream")
+	c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "")
+
+	// sign url for get object
+	str, err = s.bucket.SignURL(objectName, HTTPGet, 60)
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// get object with url
+	body, err := s.bucket.GetObjectWithURL(str)
+	c.Assert(err, IsNil)
+	str, err = readBody(body)
+	c.Assert(err, IsNil)
+	c.Assert(str, Equals, objectValue)
+
+	// sign url for put with options
+	options := []Option{
+		ObjectACL(ACLPublicRead),
+		Meta("myprop", "mypropval"),
+		ContentType("image/tiff"),
+		ResponseContentEncoding("deflate"),
+	}
+	str, err = s.bucket.SignURL(objectName, HTTPPut, 60, options...)
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// put object with url from file
+	// without option, error
+	err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue))
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+
+	err = s.bucket.PutObjectFromFileWithURL(str, filePath)
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+
+	// with option, error file
+	err = s.bucket.PutObjectFromFileWithURL(str, notExistfilePath, options...)
+	c.Assert(err, NotNil)
+
+	// with option
+	err = s.bucket.PutObjectFromFileWithURL(str, filePath, options...)
+	c.Assert(err, IsNil)
+
+	// get object meta
+	meta, err = s.bucket.GetObjectDetailedMeta(objectName)
+	c.Assert(err, IsNil)
+	c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval")
+	c.Assert(meta.Get(HTTPHeaderContentType), Equals, "image/tiff")
+
+	acl, err = s.bucket.GetObjectACL(objectName)
+	c.Assert(err, IsNil)
+	c.Assert(acl.ACL, Equals, string(ACLPublicRead))
+
+	// sign url for get object
+	str, err = s.bucket.SignURL(objectName, HTTPGet, 60)
+	c.Assert(err, IsNil)
+
+	// get object to file with url
+	newFile := randStr(10)
+	err = s.bucket.GetObjectToFileWithURL(str, newFile)
+	c.Assert(err, IsNil)
+	eq, err := compareFiles(filePath, newFile)
+	c.Assert(err, IsNil)
+	c.Assert(eq, Equals, true)
+	os.Remove(newFile)
+
+	// get object to file error
+	err = s.bucket.GetObjectToFileWithURL(str, newFile, options...)
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+	_, err = os.Stat(newFile)
+	c.Assert(err, NotNil)
+
+	// get object error
+	body, err = s.bucket.GetObjectWithURL(str, options...)
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+	c.Assert(body, IsNil)
+
+	// sign url for get object with options
+	options = []Option{
+		Expires(futureDate),
+		ObjectACL(ACLPublicRead),
+		Meta("myprop", "mypropval"),
+		ContentType("image/tiff"),
+		ResponseContentEncoding("deflate"),
+	}
+	str, err = s.bucket.SignURL(objectName, HTTPGet, 60, options...)
+	c.Assert(err, IsNil)
+
+	// get object to file with url and options
+	err = s.bucket.GetObjectToFileWithURL(str, newFile, options...)
+	c.Assert(err, IsNil)
+	eq, err = compareFiles(filePath, newFile)
+	c.Assert(err, IsNil)
+	c.Assert(eq, Equals, true)
+	os.Remove(newFile)
+
+	// get object to file error
+	err = s.bucket.GetObjectToFileWithURL(str, newFile)
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+	_, err = os.Stat(newFile)
+	c.Assert(err, NotNil)
+
+	// get object error
+	body, err = s.bucket.GetObjectWithURL(str)
+	c.Assert(err, NotNil)
+	c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch")
+	c.Assert(body, IsNil)
+
+	os.Remove(filePath)
+	os.Remove(newFile)
+
+	// sign url error
+	str, err = s.bucket.SignURL(objectName, HTTPGet, -1)
+	c.Assert(err, NotNil)
+
+	err = s.bucket.DeleteObject(objectName)
+	c.Assert(err, IsNil)
+
+	// invalid url parse
+	str = randStr(20)
+
+	err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue))
+	c.Assert(err, NotNil)
+
+	err = s.bucket.GetObjectToFileWithURL(str, newFile)
+	c.Assert(err, NotNil)
+}
+
 // TestPutObjectType
 func (s *OssBucketSuite) TestPutObjectType(c *C) {
 	objectName := objectNamePrefix + "tptt"
@@ -1332,6 +1506,21 @@ func (s *OssBucketSuite) TestAppendObject(c *C) {
 	c.Assert(err, IsNil)
 }
 
+// TestAppendObjectNegative
+func (s *OssBucketSuite) TestAppendObjectNegative(c *C) {
+	objectName := objectNamePrefix + "taon"
+	nextPos := int64(0)
+
+	nextPos, err := s.bucket.AppendObject(objectName, strings.NewReader("ObjectValue"), nextPos)
+	c.Assert(err, IsNil)
+
+	nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader("ObjectValue"), nextPos)
+	c.Assert(err, NotNil)
+
+	err = s.bucket.DeleteObject(objectName)
+	c.Assert(err, IsNil)
+}
+
 // TestContentType
 func (s *OssBucketSuite) TestAddContentType(c *C) {
 	opts := addContentType(nil, "abc.txt")

+ 63 - 26
oss/client.go

@@ -89,7 +89,7 @@ func (client Client) Bucket(bucketName string) (*Bucket, error) {
 // bucketName bucket名称,在整个OSS中具有全局唯一性,且不能修改。bucket名称的只能包括小写字母,数字和短横线-,
 // 必须以小写字母或者数字开头,长度必须在3-255字节之间。
 // options  创建bucket的选项。您可以使用选项ACL,指定bucket的访问权限。Bucket有以下三种访问权限,私有读写(ACLPrivate)、
-// 公共读私有写(ACLPublicRead),公共读公共写(ACLPublicReadWrite),默认访问权限是私有读写。可以使用StorageClass选项设置bucket的存储方式。
+// 公共读私有写(ACLPublicRead),公共读公共写(ACLPublicReadWrite),默认访问权限是私有读写。可以使用StorageClass选项设置bucket的存储方式,目前支持:标准存储模式(StorageStandard)、 低频存储模式(StorageIA)、 归档存储模式(StorageArchive)
 //
 // error 操作无错误时返回nil,非nil为错误信息。
 //
@@ -112,7 +112,8 @@ func (client Client) CreateBucket(bucketName string, options ...Option) error {
 		headers[HTTPHeaderContentType] = contentType
 	}
 
-	resp, err := client.do("PUT", bucketName, "", "", headers, buffer)
+	params := map[string]interface{}{}
+	resp, err := client.do("PUT", bucketName, params, headers, buffer)
 	if err != nil {
 		return err
 	}
@@ -134,12 +135,12 @@ func (client Client) CreateBucket(bucketName string, options ...Option) error {
 func (client Client) ListBuckets(options ...Option) (ListBucketsResult, error) {
 	var out ListBucketsResult
 
-	params, err := handleParams(options)
+	params, err := getRawParams(options)
 	if err != nil {
 		return out, err
 	}
 
-	resp, err := client.do("GET", "", params, "", nil, nil)
+	resp, err := client.do("GET", "", params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -177,7 +178,8 @@ func (client Client) IsBucketExist(bucketName string) (bool, error) {
 // error 操作无错误时返回nil,非nil为错误信息。
 //
 func (client Client) DeleteBucket(bucketName string) error {
-	resp, err := client.do("DELETE", bucketName, "", "", nil, nil)
+	params := map[string]interface{}{}
+	resp, err := client.do("DELETE", bucketName, params, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -198,7 +200,9 @@ func (client Client) DeleteBucket(bucketName string) error {
 // error  操作无错误时返回nil,非nil为错误信息。
 //
 func (client Client) GetBucketLocation(bucketName string) (string, error) {
-	resp, err := client.do("GET", bucketName, "location", "location", nil, nil)
+	params := map[string]interface{}{}
+	params["location"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return "", err
 	}
@@ -220,7 +224,8 @@ func (client Client) GetBucketLocation(bucketName string) (string, error) {
 //
 func (client Client) SetBucketACL(bucketName string, bucketACL ACLType) error {
 	headers := map[string]string{HTTPHeaderOssACL: string(bucketACL)}
-	resp, err := client.do("PUT", bucketName, "", "", headers, nil)
+	params := map[string]interface{}{}
+	resp, err := client.do("PUT", bucketName, params, headers, nil)
 	if err != nil {
 		return err
 	}
@@ -238,7 +243,9 @@ func (client Client) SetBucketACL(bucketName string, bucketACL ACLType) error {
 //
 func (client Client) GetBucketACL(bucketName string) (GetBucketACLResult, error) {
 	var out GetBucketACLResult
-	resp, err := client.do("GET", bucketName, "acl", "acl", nil, nil)
+	params := map[string]interface{}{}
+	params["acl"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -275,7 +282,9 @@ func (client Client) SetBucketLifecycle(bucketName string, rules []LifecycleRule
 	headers := map[string]string{}
 	headers[HTTPHeaderContentType] = contentType
 
-	resp, err := client.do("PUT", bucketName, "lifecycle", "lifecycle", headers, buffer)
+	params := map[string]interface{}{}
+	params["lifecycle"] = nil
+	resp, err := client.do("PUT", bucketName, params, headers, buffer)
 	if err != nil {
 		return err
 	}
@@ -292,7 +301,9 @@ func (client Client) SetBucketLifecycle(bucketName string, rules []LifecycleRule
 // error 操作无错误为nil,非nil为错误信息。
 //
 func (client Client) DeleteBucketLifecycle(bucketName string) error {
-	resp, err := client.do("DELETE", bucketName, "lifecycle", "lifecycle", nil, nil)
+	params := map[string]interface{}{}
+	params["lifecycle"] = nil
+	resp, err := client.do("DELETE", bucketName, params, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -310,7 +321,9 @@ func (client Client) DeleteBucketLifecycle(bucketName string) error {
 //
 func (client Client) GetBucketLifecycle(bucketName string) (GetBucketLifecycleResult, error) {
 	var out GetBucketLifecycleResult
-	resp, err := client.do("GET", bucketName, "lifecycle", "lifecycle", nil, nil)
+	params := map[string]interface{}{}
+	params["lifecycle"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -358,7 +371,9 @@ func (client Client) SetBucketReferer(bucketName string, referers []string, allo
 	headers := map[string]string{}
 	headers[HTTPHeaderContentType] = contentType
 
-	resp, err := client.do("PUT", bucketName, "referer", "referer", headers, buffer)
+	params := map[string]interface{}{}
+	params["referer"] = nil
+	resp, err := client.do("PUT", bucketName, params, headers, buffer)
 	if err != nil {
 		return err
 	}
@@ -376,7 +391,9 @@ func (client Client) SetBucketReferer(bucketName string, referers []string, allo
 //
 func (client Client) GetBucketReferer(bucketName string) (GetBucketRefererResult, error) {
 	var out GetBucketRefererResult
-	resp, err := client.do("GET", bucketName, "referer", "referer", nil, nil)
+	params := map[string]interface{}{}
+	params["referer"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -424,7 +441,9 @@ func (client Client) SetBucketLogging(bucketName, targetBucket, targetPrefix str
 	headers := map[string]string{}
 	headers[HTTPHeaderContentType] = contentType
 
-	resp, err := client.do("PUT", bucketName, "logging", "logging", headers, buffer)
+	params := map[string]interface{}{}
+	params["logging"] = nil
+	resp, err := client.do("PUT", bucketName, params, headers, buffer)
 	if err != nil {
 		return err
 	}
@@ -440,7 +459,9 @@ func (client Client) SetBucketLogging(bucketName, targetBucket, targetPrefix str
 // error 操作无错误为nil,非nil为错误信息。
 //
 func (client Client) DeleteBucketLogging(bucketName string) error {
-	resp, err := client.do("DELETE", bucketName, "logging", "logging", nil, nil)
+	params := map[string]interface{}{}
+	params["logging"] = nil
+	resp, err := client.do("DELETE", bucketName, params, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -458,7 +479,9 @@ func (client Client) DeleteBucketLogging(bucketName string) error {
 //
 func (client Client) GetBucketLogging(bucketName string) (GetBucketLoggingResult, error) {
 	var out GetBucketLoggingResult
-	resp, err := client.do("GET", bucketName, "logging", "logging", nil, nil)
+	params := map[string]interface{}{}
+	params["logging"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -496,7 +519,9 @@ func (client Client) SetBucketWebsite(bucketName, indexDocument, errorDocument s
 	headers := make(map[string]string)
 	headers[HTTPHeaderContentType] = contentType
 
-	resp, err := client.do("PUT", bucketName, "website", "website", headers, buffer)
+	params := map[string]interface{}{}
+	params["website"] = nil
+	resp, err := client.do("PUT", bucketName, params, headers, buffer)
 	if err != nil {
 		return err
 	}
@@ -512,7 +537,9 @@ func (client Client) SetBucketWebsite(bucketName, indexDocument, errorDocument s
 // error  操作无错误为nil,非nil为错误信息。
 //
 func (client Client) DeleteBucketWebsite(bucketName string) error {
-	resp, err := client.do("DELETE", bucketName, "website", "website", nil, nil)
+	params := map[string]interface{}{}
+	params["website"] = nil
+	resp, err := client.do("DELETE", bucketName, params, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -530,7 +557,9 @@ func (client Client) DeleteBucketWebsite(bucketName string) error {
 //
 func (client Client) GetBucketWebsite(bucketName string) (GetBucketWebsiteResult, error) {
 	var out GetBucketWebsiteResult
-	resp, err := client.do("GET", bucketName, "website", "website", nil, nil)
+	params := map[string]interface{}{}
+	params["website"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -573,7 +602,9 @@ func (client Client) SetBucketCORS(bucketName string, corsRules []CORSRule) erro
 	headers := map[string]string{}
 	headers[HTTPHeaderContentType] = contentType
 
-	resp, err := client.do("PUT", bucketName, "cors", "cors", headers, buffer)
+	params := map[string]interface{}{}
+	params["cors"] = nil
+	resp, err := client.do("PUT", bucketName, params, headers, buffer)
 	if err != nil {
 		return err
 	}
@@ -589,7 +620,9 @@ func (client Client) SetBucketCORS(bucketName string, corsRules []CORSRule) erro
 // error 操作无错误为nil,非nil为错误信息。
 //
 func (client Client) DeleteBucketCORS(bucketName string) error {
-	resp, err := client.do("DELETE", bucketName, "cors", "cors", nil, nil)
+	params := map[string]interface{}{}
+	params["cors"] = nil
+	resp, err := client.do("DELETE", bucketName, params, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -608,7 +641,9 @@ func (client Client) DeleteBucketCORS(bucketName string) error {
 //
 func (client Client) GetBucketCORS(bucketName string) (GetBucketCORSResult, error) {
 	var out GetBucketCORSResult
-	resp, err := client.do("GET", bucketName, "cors", "cors", nil, nil)
+	params := map[string]interface{}{}
+	params["cors"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -628,7 +663,9 @@ func (client Client) GetBucketCORS(bucketName string) (GetBucketCORSResult, erro
 //
 func (client Client) GetBucketInfo(bucketName string) (GetBucketInfoResult, error) {
 	var out GetBucketInfoResult
-	resp, err := client.do("GET", bucketName, "bucketInfo", "bucketInfo", nil, nil)
+	params := map[string]interface{}{}
+	params["bucketInfo"] = nil
+	resp, err := client.do("GET", bucketName, params, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -756,8 +793,8 @@ func AuthProxy(proxyHost, proxyUser, proxyPassword string) ClientOption {
 }
 
 // Private
-func (client Client) do(method, bucketName, urlParams, subResource string,
+func (client Client) do(method, bucketName string, params map[string]interface{},
 	headers map[string]string, data io.Reader) (*Response, error) {
-	return client.Conn.Do(method, bucketName, "", urlParams,
-		subResource, headers, data, 0, nil)
+	return client.Conn.Do(method, bucketName, "", params,
+		headers, data, 0, nil)
 }

+ 33 - 0
oss/client_test.go

@@ -69,6 +69,14 @@ func randStr(n int) string {
 	return string(b)
 }
 
+func createFile(fileName, content string, c *C) {
+	fout, err := os.Create(fileName)
+	defer fout.Close()
+	c.Assert(err, IsNil)
+	_, err = fout.WriteString(content)
+	c.Assert(err, IsNil)
+}
+
 func randLowStr(n int) string {
 	return strings.ToLower(randStr(n))
 }
@@ -1431,6 +1439,31 @@ func (s *OssClientSuite) TestProxy(c *C) {
 
 	bucket, err := client.Bucket(bucketNameTest)
 
+	// Sign url
+	str, err := bucket.SignURL(objectName, HTTPPut, 60)
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// Put object with url
+	err = bucket.PutObjectWithURL(str, strings.NewReader(objectValue))
+	c.Assert(err, IsNil)
+
+	// sign url for get object
+	str, err = bucket.SignURL(objectName, HTTPGet, 60)
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// Get object with url
+	body, err := bucket.GetObjectWithURL(str)
+	c.Assert(err, IsNil)
+	str, err = readBody(body)
+	c.Assert(err, IsNil)
+	c.Assert(str, Equals, objectValue)
+
 	// Put object
 	err = bucket.PutObject(objectName, strings.NewReader(objectValue))
 	c.Assert(err, IsNil)

+ 169 - 1
oss/conn.go

@@ -13,6 +13,7 @@ import (
 	"net/http"
 	"net/url"
 	"os"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -25,6 +26,8 @@ type Conn struct {
 	client *http.Client
 }
 
+var signKeyList = []string{"acl", "uploads", "location", "cors", "logging", "website", "referer", "lifecycle", "delete", "append", "tagging", "objectMeta", "uploadId", "partNumber", "security-token", "position", "img", "style", "styleName", "replication", "replicationProgress", "replicationLocation", "cname", "bucketInfo", "comp", "qos", "live", "status", "vod", "startTime", "endTime", "symlink", "x-oss-process", "response-content-type", "response-content-language", "response-expires", "response-cache-control", "response-content-disposition", "response-content-encoding", "udf", "udfName", "udfImage", "udfId", "udfImageDesc", "udfApplication", "comp", "udfApplicationLog", "restore"}
+
 // init 初始化Conn
 func (conn *Conn) init(config *Config, urlMaker *urlMaker) error {
 	httpTimeOut := conn.config.HTTPTimeout
@@ -58,13 +61,138 @@ func (conn *Conn) init(config *Config, urlMaker *urlMaker) error {
 }
 
 // Do 处理请求,返回响应结果。
-func (conn Conn) Do(method, bucketName, objectName, urlParams, subResource string, headers map[string]string,
+func (conn Conn) Do(method, bucketName, objectName string, params map[string]interface{}, headers map[string]string,
 	data io.Reader, initCRC uint64, listener ProgressListener) (*Response, error) {
+	urlParams := conn.getURLParams(params)
+	subResource := conn.getSubResource(params)
 	uri := conn.url.getURL(bucketName, objectName, urlParams)
 	resource := conn.url.getResource(bucketName, objectName, subResource)
 	return conn.doRequest(method, uri, resource, headers, data, initCRC, listener)
 }
 
+// DoURL 根据已签名的URL处理请求,返回响应结果。
+func (conn Conn) DoURL(method HTTPMethod, signedURL string, headers map[string]string,
+	data io.Reader, initCRC uint64, listener ProgressListener) (*Response, error) {
+	// get uri form signedURL
+	uri, err := url.ParseRequestURI(signedURL)
+	if err != nil {
+		return nil, err
+	}
+
+	m := strings.ToUpper(string(method))
+	if !conn.config.IsUseProxy {
+		uri.Opaque = uri.Path
+	}
+	req := &http.Request{
+		Method:     m,
+		URL:        uri,
+		Proto:      "HTTP/1.1",
+		ProtoMajor: 1,
+		ProtoMinor: 1,
+		Header:     make(http.Header),
+		Host:       uri.Host,
+	}
+
+	tracker := &readerTracker{completedBytes: 0}
+	fd, crc := conn.handleBody(req, data, initCRC, listener, tracker)
+	if fd != nil {
+		defer func() {
+			fd.Close()
+			os.Remove(fd.Name())
+		}()
+	}
+
+	if conn.config.IsAuthProxy {
+		auth := conn.config.ProxyUser + ":" + conn.config.ProxyPassword
+		basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
+		req.Header.Set("Proxy-Authorization", basic)
+	}
+
+	req.Header.Set(HTTPHeaderHost, conn.config.Endpoint)
+	req.Header.Set(HTTPHeaderUserAgent, conn.config.UserAgent)
+
+	if headers != nil {
+		for k, v := range headers {
+			req.Header.Set(k, v)
+		}
+	}
+
+	// transfer started
+	event := newProgressEvent(TransferStartedEvent, 0, req.ContentLength)
+	publishProgress(listener, event)
+
+	resp, err := conn.client.Do(req)
+	if err != nil {
+		// transfer failed
+		event = newProgressEvent(TransferFailedEvent, tracker.completedBytes, req.ContentLength)
+		publishProgress(listener, event)
+		return nil, err
+	}
+
+	// transfer completed
+	event = newProgressEvent(TransferCompletedEvent, tracker.completedBytes, req.ContentLength)
+	publishProgress(listener, event)
+
+	return conn.handleResponse(resp, crc)
+}
+
+func (conn Conn) getURLParams(params map[string]interface{}) string {
+	// sort
+	keys := make([]string, 0, len(params))
+	for k := range params {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	// serialize
+	var buf bytes.Buffer
+	for _, k := range keys {
+		if buf.Len() > 0 {
+			buf.WriteByte('&')
+		}
+		buf.WriteString(url.QueryEscape(k))
+		if params[k] != nil {
+			buf.WriteString("=" + url.QueryEscape(params[k].(string)))
+		}
+	}
+
+	return buf.String()
+}
+
+func (conn Conn) getSubResource(params map[string]interface{}) string {
+	// sort
+	keys := make([]string, 0, len(params))
+	for k := range params {
+		if conn.isParamSign(k) {
+			keys = append(keys, k)
+		}
+	}
+	sort.Strings(keys)
+
+	// serialize
+	var buf bytes.Buffer
+	for _, k := range keys {
+		if buf.Len() > 0 {
+			buf.WriteByte('&')
+		}
+		buf.WriteString(k)
+		if params[k] != nil {
+			buf.WriteString("=" + params[k].(string))
+		}
+	}
+
+	return buf.String()
+}
+
+func (conn Conn) isParamSign(paramKey string) bool {
+	for _, k := range signKeyList {
+		if paramKey == k {
+			return true
+		}
+	}
+	return false
+}
+
 func (conn Conn) doRequest(method string, uri *url.URL, canonicalizedResource string, headers map[string]string,
 	data io.Reader, initCRC uint64, listener ProgressListener) (*Response, error) {
 	method = strings.ToUpper(method)
@@ -131,6 +259,45 @@ func (conn Conn) doRequest(method string, uri *url.URL, canonicalizedResource st
 	return conn.handleResponse(resp, crc)
 }
 
+func (conn Conn) signURL(method HTTPMethod, bucketName, objectName string, expiration int64, params map[string]interface{}, headers map[string]string) string {
+	subResource := conn.getSubResource(params)
+	canonicalizedResource := conn.url.getResource(bucketName, objectName, subResource)
+
+	m := strings.ToUpper(string(method))
+	req := &http.Request{
+		Method: m,
+		Header: make(http.Header),
+	}
+
+	if conn.config.IsAuthProxy {
+		auth := conn.config.ProxyUser + ":" + conn.config.ProxyPassword
+		basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
+		req.Header.Set("Proxy-Authorization", basic)
+	}
+
+	req.Header.Set(HTTPHeaderDate, strconv.FormatInt(expiration, 10))
+	req.Header.Set(HTTPHeaderHost, conn.config.Endpoint)
+	req.Header.Set(HTTPHeaderUserAgent, conn.config.UserAgent)
+
+	if headers != nil {
+		for k, v := range headers {
+			req.Header.Set(k, v)
+		}
+	}
+
+	signedStr := conn.getSignedStr(req, canonicalizedResource)
+
+	params[HTTPParamExpires] = strconv.FormatInt(expiration, 10)
+	params[HTTPParamAccessKeyID] = conn.config.AccessKeyID
+	params[HTTPParamSignature] = signedStr
+	if conn.config.SecurityToken != "" {
+		params[HTTPParamSecurityToken] = conn.config.SecurityToken
+	}
+
+	urlParams := conn.getURLParams(params)
+	return conn.url.getURL(bucketName, objectName, urlParams).String()
+}
+
 // handle request body
 func (conn Conn) handleBody(req *http.Request, body io.Reader, initCRC uint64,
 	listener ProgressListener, tracker *readerTracker) (*os.File, hash.Hash64) {
@@ -207,6 +374,7 @@ func (conn Conn) handleResponse(resp *http.Response, crc hash.Hash64) (*Response
 			}
 			err = srvErr
 		}
+
 		return &Response{
 			StatusCode: resp.StatusCode,
 			Headers:    resp.Header,

+ 31 - 3
oss/const.go

@@ -37,13 +37,33 @@ const (
 	// StorageStandard 标准存储模式
 	StorageStandard StorageClassType = "Standard"
 
-	// StorageIA IA存储模式
+	// StorageIA 低频存储模式
 	StorageIA StorageClassType = "IA"
 
-	// StorageArchive Archive存储模式
+	// StorageArchive 归档存储模式
 	StorageArchive StorageClassType = "Archive"
 )
 
+// HTTPMethod HTTP请求方法
+type HTTPMethod string
+
+const (
+	// HTTPGet HTTP请求方法 GET
+	HTTPGet HTTPMethod = "GET"
+
+	// HTTPPut HTTP请求方法 PUT
+	HTTPPut HTTPMethod = "PUT"
+
+	// HTTPHead HTTP请求方法 HEAD
+	HTTPHead HTTPMethod = "HEAD"
+
+	// HTTPPost HTTP请求方法 POST
+	HTTPPost HTTPMethod = "POST"
+
+	// HTTPDelete HTTP请求方法 DELETE
+	HTTPDelete HTTPMethod = "DELETE"
+)
+
 // Http头标签
 const (
 	HTTPHeaderAcceptEncoding     string = "Accept-Encoding"
@@ -88,6 +108,14 @@ const (
 	HTTPHeaderOssSymlinkTarget               = "X-Oss-Symlink-Target"
 )
 
+// Http Param
+const (
+	HTTPParamExpires       = "Expires"
+	HTTPParamAccessKeyID   = "OSSAccessKeyId"
+	HTTPParamSignature     = "Signature"
+	HTTPParamSecurityToken = "security-token"
+)
+
 // 其它常量
 const (
 	MaxPartSize = 5 * 1024 * 1024 * 1024 // 文件片最大值,5GB
@@ -100,5 +128,5 @@ const (
 
 	CheckpointFileSuffix = ".cp" // Checkpoint文件后缀
 
-	Version = "1.4.0" // Go sdk版本
+	Version = "1.5.0" // Go sdk版本
 )

+ 2 - 2
oss/download.go

@@ -22,8 +22,8 @@ import (
 // error 操作成功error为nil,非nil为错误信息。
 //
 func (bucket Bucket) DownloadFile(objectKey, filePath string, partSize int64, options ...Option) error {
-	if partSize < 1 || partSize > MaxPartSize {
-		return errors.New("oss: part size invalid range (1, 5GB]")
+	if partSize < 1 {
+		return errors.New("oss: part size smaller than 1.")
 	}
 
 	cpConf, err := getCpConfig(options, filePath)

+ 1 - 1
oss/download_test.go

@@ -329,7 +329,7 @@ func (s *OssDownloadSuite) TestDownloadNegative(c *C) {
 	c.Assert(err, NotNil)
 
 	err = s.bucket.DownloadFile(objectName, newFile, 1024*1024*1024*100, Routines(2))
-	c.Assert(err, NotNil)
+	c.Assert(err, IsNil)
 
 	err = s.bucket.DeleteObject(objectName)
 	c.Assert(err, IsNil)

+ 23 - 13
oss/multipart.go

@@ -24,7 +24,9 @@ import (
 func (bucket Bucket) InitiateMultipartUpload(objectKey string, options ...Option) (InitiateMultipartUploadResult, error) {
 	var imur InitiateMultipartUploadResult
 	opts := addContentType(options, objectKey)
-	resp, err := bucket.do("POST", objectKey, "uploads", "uploads", opts, nil, nil)
+	params := map[string]interface{}{}
+	params["uploads"] = nil
+	resp, err := bucket.do("POST", objectKey, params, opts, nil, nil)
 	if err != nil {
 		return imur, err
 	}
@@ -111,9 +113,11 @@ func (bucket Bucket) UploadPartFromFile(imur InitiateMultipartUploadResult, file
 //
 func (bucket Bucket) DoUploadPart(request *UploadPartRequest, options []Option) (*UploadPartResult, error) {
 	listener := getProgressListener(options)
-	params := "partNumber=" + strconv.Itoa(request.PartNumber) + "&uploadId=" + request.InitResult.UploadID
 	opts := []Option{ContentLength(request.PartSize)}
-	resp, err := bucket.do("PUT", request.InitResult.Key, params, params, opts,
+	params := map[string]interface{}{}
+	params["partNumber"] = strconv.Itoa(request.PartNumber)
+	params["uploadId"] = request.InitResult.UploadID
+	resp, err := bucket.do("PUT", request.InitResult.Key, params, opts,
 		&io.LimitedReader{R: request.Reader, N: request.PartSize}, listener)
 	if err != nil {
 		return &UploadPartResult{}, err
@@ -159,8 +163,10 @@ func (bucket Bucket) UploadPartCopy(imur InitiateMultipartUploadResult, srcBucke
 	opts := []Option{CopySource(srcBucketName, srcObjectKey),
 		CopySourceRange(startPosition, partSize)}
 	opts = append(opts, options...)
-	params := "partNumber=" + strconv.Itoa(partNumber) + "&uploadId=" + imur.UploadID
-	resp, err := bucket.do("PUT", imur.Key, params, params, opts, nil, nil)
+	params := map[string]interface{}{}
+	params["partNumber"] = strconv.Itoa(partNumber)
+	params["uploadId"] = imur.UploadID
+	resp, err := bucket.do("PUT", imur.Key, params, opts, nil, nil)
 	if err != nil {
 		return part, err
 	}
@@ -199,8 +205,9 @@ func (bucket Bucket) CompleteMultipartUpload(imur InitiateMultipartUploadResult,
 	buffer := new(bytes.Buffer)
 	buffer.Write(bs)
 
-	params := "uploadId=" + imur.UploadID
-	resp, err := bucket.do("POST", imur.Key, params, params, nil, buffer, nil)
+	params := map[string]interface{}{}
+	params["uploadId"] = imur.UploadID
+	resp, err := bucket.do("POST", imur.Key, params, nil, buffer, nil)
 	if err != nil {
 		return out, err
 	}
@@ -218,8 +225,9 @@ func (bucket Bucket) CompleteMultipartUpload(imur InitiateMultipartUploadResult,
 // error  操作成功error为nil,非nil为错误信息。
 //
 func (bucket Bucket) AbortMultipartUpload(imur InitiateMultipartUploadResult) error {
-	params := "uploadId=" + imur.UploadID
-	resp, err := bucket.do("DELETE", imur.Key, params, params, nil, nil, nil)
+	params := map[string]interface{}{}
+	params["uploadId"] = imur.UploadID
+	resp, err := bucket.do("DELETE", imur.Key, params, nil, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -237,8 +245,9 @@ func (bucket Bucket) AbortMultipartUpload(imur InitiateMultipartUploadResult) er
 //
 func (bucket Bucket) ListUploadedParts(imur InitiateMultipartUploadResult) (ListUploadedPartsResult, error) {
 	var out ListUploadedPartsResult
-	params := "uploadId=" + imur.UploadID
-	resp, err := bucket.do("GET", imur.Key, params, params, nil, nil, nil)
+	params := map[string]interface{}{}
+	params["uploadId"] = imur.UploadID
+	resp, err := bucket.do("GET", imur.Key, params, nil, nil, nil)
 	if err != nil {
 		return out, err
 	}
@@ -261,12 +270,13 @@ func (bucket Bucket) ListMultipartUploads(options ...Option) (ListMultipartUploa
 	var out ListMultipartUploadResult
 
 	options = append(options, EncodingType("url"))
-	params, err := handleParams(options)
+	params, err := getRawParams(options)
 	if err != nil {
 		return out, err
 	}
+	params["uploads"] = nil
 
-	resp, err := bucket.do("GET", "", "uploads&"+params, "uploads", nil, nil, nil)
+	resp, err := bucket.do("GET", "", params, nil, nil, nil)
 	if err != nil {
 		return out, err
 	}

+ 41 - 23
oss/option.go

@@ -1,11 +1,8 @@
 package oss
 
 import (
-	"bytes"
 	"fmt"
 	"net/http"
-	"net/url"
-	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -251,6 +248,40 @@ func Progress(listener ProgressListener) Option {
 	return addArg(progressListener, listener)
 }
 
+// ResponseContentType is an option to set response-content-type param
+func ResponseContentType(value string) Option {
+	return addParam("response-content-type", value)
+}
+
+// ResponseContentLanguage is an option to set response-content-language param
+func ResponseContentLanguage(value string) Option {
+	return addParam("response-content-language", value)
+}
+
+// ResponseExpires is an option to set response-expires param
+func ResponseExpires(value string) Option {
+	return addParam("response-expires", value)
+}
+
+// ResponseCacheControl is an option to set response-cache-control param
+func ResponseCacheControl(value string) Option {
+	return addParam("response-cache-control", value)
+}
+
+// ResponseContentDisposition is an option to set response-content-disposition param
+func ResponseContentDisposition(value string) Option {
+	return addParam("response-content-disposition", value)
+}
+
+// ResponseContentEncoding is an option to set response-content-encoding param
+func ResponseContentEncoding(value string) Option {
+	return addParam("response-content-encoding", value)
+}
+
+// Process is an option to set X-Oss-Process param
+func Process(value string) Option {
+	return addParam("X-Oss-Process", value)
+}
 func setHeader(key string, value interface{}) Option {
 	return func(params map[string]optionValue) error {
 		if value == nil {
@@ -299,40 +330,27 @@ func handleOptions(headers map[string]string, options []Option) error {
 	return nil
 }
 
-func handleParams(options []Option) (string, error) {
+func getRawParams(options []Option) (map[string]interface{}, error) {
 	// option
 	params := map[string]optionValue{}
 	for _, option := range options {
 		if option != nil {
 			if err := option(params); err != nil {
-				return "", err
+				return nil, err
 			}
 		}
 	}
 
-	// sort
-	var buf bytes.Buffer
-	keys := make([]string, 0, len(params))
+	paramsm := map[string]interface{}{}
+	// serialize
 	for k, v := range params {
 		if v.Type == optionParam {
-			keys = append(keys, k)
-		}
-	}
-	sort.Strings(keys)
-
-	// serialize
-	for _, k := range keys {
-		vs := params[k]
-		prefix := url.QueryEscape(k) + "="
-
-		if buf.Len() > 0 {
-			buf.WriteByte('&')
+			vs := params[k]
+			paramsm[k] = vs.Value.(string)
 		}
-		buf.WriteString(prefix)
-		buf.WriteString(url.QueryEscape(vs.Value.(string)))
 	}
 
-	return buf.String(), nil
+	return paramsm, nil
 }
 
 func findOption(options []Option, param string, defaultVal interface{}) (interface{}, error) {

+ 11 - 3
oss/option_test.go

@@ -218,20 +218,28 @@ func (s *OssOptionSuite) TestHandleOptions(c *C) {
 }
 
 func (s *OssOptionSuite) TestHandleParams(c *C) {
+	client, err := New(endpoint, accessID, accessKey)
+	c.Assert(err, IsNil)
+
 	options := []Option{}
 
 	for _, testcase := range paramTestCases {
 		options = append(options, testcase.option)
 	}
 
-	out, err := handleParams(options)
+	params, err := getRawParams(options)
 	c.Assert(err, IsNil)
+
+	out := client.Conn.getURLParams(params)
 	c.Assert(len(out), Equals, 120)
 
 	options = []Option{KeyMarker(""), nil}
-	out, err = handleParams(options)
-	c.Assert(out, Equals, "key-marker=")
+
+	params, err = getRawParams(options)
 	c.Assert(err, IsNil)
+
+	out = client.Conn.getURLParams(params)
+	c.Assert(out, Equals, "key-marker=")
 }
 
 func (s *OssOptionSuite) TestFindOption(c *C) {

+ 69 - 0
oss/progress_test.go

@@ -145,6 +145,75 @@ func (s *OssProgressSuite) TestPutObject(c *C) {
 	testLogger.Println("OssProgressSuite.TestPutObject")
 }
 
+// Test SignURL
+func (s *OssProgressSuite) TestSignURL(c *C) {
+	objectName := objectNamePrefix + randStr(5)
+	filePath := randLowStr(10)
+	content := randStr(20)
+	createFile(filePath, content, c)
+
+	// sign url for put
+	str, err := s.bucket.SignURL(objectName, HTTPPut, 60, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// put object with url
+	fd, err := os.Open(filePath)
+	c.Assert(err, IsNil)
+	defer fd.Close()
+
+	err = s.bucket.PutObjectWithURL(str, fd, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+
+	// put object from file with url
+	err = s.bucket.PutObjectFromFileWithURL(str, filePath, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+
+	// DoPutObject
+	fd, err = os.Open(filePath)
+	c.Assert(err, IsNil)
+	defer fd.Close()
+
+	options := []Option{Progress(&OssProgressListener{})}
+	_, err = s.bucket.DoPutObjectWithURL(str, fd, options)
+	c.Assert(err, IsNil)
+
+	// sign url for get
+	str, err = s.bucket.SignURL(objectName, HTTPGet, 60, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+	c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true)
+	c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true)
+
+	// get object with url
+	body, err := s.bucket.GetObjectWithURL(str, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+	str, err = readBody(body)
+	c.Assert(err, IsNil)
+	c.Assert(str, Equals, content)
+
+	// get object to file with url
+	str, err = s.bucket.SignURL(objectName, HTTPGet, 10, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+
+	newFile := randStr(10)
+	err = s.bucket.GetObjectToFileWithURL(str, newFile, Progress(&OssProgressListener{}))
+	c.Assert(err, IsNil)
+	eq, err := compareFiles(filePath, newFile)
+	c.Assert(err, IsNil)
+	c.Assert(eq, Equals, true)
+
+	os.Remove(filePath)
+	os.Remove(newFile)
+
+	err = s.bucket.DeleteObject(objectName)
+	c.Assert(err, IsNil)
+
+	testLogger.Println("OssProgressSuite.TestSignURL")
+}
+
 func (s *OssProgressSuite) TestPutObjectNegative(c *C) {
 	objectName := objectNamePrefix + "tpon.html"
 	localFile := "../sample/The Go Programming Language.html"