diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 17428002..d21f5260 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -44,6 +44,14 @@ "Comment": "1.2.0-66-g6086d79", "Rev": "6086d7927ec35315964d9fea46df6c04e6d697c1" }, + { + "ImportPath": "github.com/denverdino/aliyungo/oss", + "Rev": "17d1e888c907ffdbd875f37500f3d130ce2ee6eb" + }, + { + "ImportPath": "github.com/denverdino/aliyungo/util", + "Rev": "17d1e888c907ffdbd875f37500f3d130ce2ee6eb" + }, { "ImportPath": "github.com/docker/docker/pkg/tarsum", "Comment": "v1.4.1-3932-gb63ec6e", diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/client.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/client.go new file mode 100644 index 00000000..8e38ea4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/client.go @@ -0,0 +1,1252 @@ +package oss + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "encoding/base64" + "encoding/xml" + "fmt" + "github.com/denverdino/aliyungo/util" + "io" + "io/ioutil" + "log" + "mime" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path" + "strconv" + "strings" + "time" +) + +const DefaultContentType = "application/octet-stream" + +// The Client type encapsulates operations with an OSS region. +type Client struct { + AccessKeyId string + AccessKeySecret string + Region Region + Internal bool + Secure bool + ConnectTimeout time.Duration + ReadTimeout time.Duration + debug bool +} + +// The Bucket type encapsulates operations with an bucket. +type Bucket struct { + *Client + Name string +} + +// The Owner type represents the owner of the object in an bucket. +type Owner struct { + ID string + DisplayName string +} + +// Options struct +// +type Options struct { + ServerSideEncryption bool + Meta map[string][]string + ContentEncoding string + CacheControl string + ContentMD5 string + ContentDisposition string + //Range string + //Expires int +} + +type CopyOptions struct { + Headers http.Header + CopySourceOptions string + MetadataDirective string + //ContentType string +} + +// CopyObjectResult is the output from a Copy request +type CopyObjectResult struct { + ETag string + LastModified string +} + +var attempts = util.AttemptStrategy{ + Min: 5, + Total: 5 * time.Second, + Delay: 200 * time.Millisecond, +} + +// NewOSSClient creates a new OSS. + +func NewOSSClient(region Region, internal bool, accessKeyId string, accessKeySecret string, secure bool) *Client { + return &Client{ + AccessKeyId: accessKeyId, + AccessKeySecret: accessKeySecret, + Region: region, + Internal: internal, + debug: false, + Secure: secure, + } +} + +// SetDebug sets debug mode to log the request/response message +func (client *Client) SetDebug(debug bool) { + client.debug = debug +} + +// Bucket returns a Bucket with the given name. +func (client *Client) Bucket(name string) *Bucket { + name = strings.ToLower(name) + return &Bucket{ + Client: client, + Name: name, + } +} + +type BucketInfo struct { + Name string + CreationDate string +} + +type GetServiceResp struct { + Owner Owner + Buckets []BucketInfo `xml:">Bucket"` +} + +// GetService gets a list of all buckets owned by an account. +func (client *Client) GetService() (*GetServiceResp, error) { + bucket := client.Bucket("") + + r, err := bucket.Get("") + if err != nil { + return nil, err + } + + // Parse the XML response. + var resp GetServiceResp + if err = xml.Unmarshal(r, &resp); err != nil { + return nil, err + } + + return &resp, nil +} + +type ACL string + +const ( + Private = ACL("private") + PublicRead = ACL("public-read") + PublicReadWrite = ACL("public-read-write") + AuthenticatedRead = ACL("authenticated-read") + BucketOwnerRead = ACL("bucket-owner-read") + BucketOwnerFull = ACL("bucket-owner-full-control") +) + +var createBucketConfiguration = ` + %s +` + +// locationConstraint returns an io.Reader specifying a LocationConstraint if +// required for the region. +func (client *Client) locationConstraint() io.Reader { + constraint := fmt.Sprintf(createBucketConfiguration, client.Region) + return strings.NewReader(constraint) +} + +// PutBucket creates a new bucket. +func (b *Bucket) PutBucket(perm ACL) error { + headers := make(http.Header) + if perm != "" { + headers.Set("x-oss-acl", string(perm)) + } + req := &request{ + method: "PUT", + bucket: b.Name, + path: "/", + headers: headers, + payload: b.Client.locationConstraint(), + } + return b.Client.query(req, nil) +} + +// DelBucket removes an existing bucket. All objects in the bucket must +// be removed before the bucket itself can be removed. +func (b *Bucket) DelBucket() (err error) { + req := &request{ + method: "DELETE", + bucket: b.Name, + path: "/", + } + for attempt := attempts.Start(); attempt.Next(); { + err = b.Client.query(req, nil) + if !shouldRetry(err) { + break + } + } + return err +} + +// Get retrieves an object from an bucket. +func (b *Bucket) Get(path string) (data []byte, err error) { + body, err := b.GetReader(path) + if err != nil { + return nil, err + } + data, err = ioutil.ReadAll(body) + body.Close() + return data, err +} + +// GetReader retrieves an object from an bucket, +// returning the body of the HTTP response. +// It is the caller's responsibility to call Close on rc when +// finished reading. +func (b *Bucket) GetReader(path string) (rc io.ReadCloser, err error) { + resp, err := b.GetResponse(path) + if resp != nil { + return resp.Body, err + } + return nil, err +} + +// GetResponse retrieves an object from an bucket, +// returning the HTTP response. +// It is the caller's responsibility to call Close on rc when +// finished reading +func (b *Bucket) GetResponse(path string) (resp *http.Response, err error) { + return b.GetResponseWithHeaders(path, make(http.Header)) +} + +// GetResponseWithHeaders retrieves an object from an bucket +// Accepts custom headers to be sent as the second parameter +// returning the body of the HTTP response. +// It is the caller's responsibility to call Close on rc when +// finished reading +func (b *Bucket) GetResponseWithHeaders(path string, headers http.Header) (resp *http.Response, err error) { + req := &request{ + bucket: b.Name, + path: path, + headers: headers, + } + err = b.Client.prepare(req) + if err != nil { + return nil, err + } + for attempt := attempts.Start(); attempt.Next(); { + resp, err := b.Client.run(req, nil) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return nil, err + } + return resp, nil + } + panic("unreachable") +} + +// Get retrieves an object from an bucket. +func (b *Bucket) GetWithParams(path string, params url.Values) (data []byte, err error) { + resp, err := b.GetResponseWithParamsAndHeaders(path, params, nil) + if err != nil { + return nil, err + } + data, err = ioutil.ReadAll(resp.Body) + resp.Body.Close() + return data, err +} + +func (b *Bucket) GetResponseWithParamsAndHeaders(path string, params url.Values, headers http.Header) (resp *http.Response, err error) { + req := &request{ + bucket: b.Name, + path: path, + params: params, + headers: headers, + } + err = b.Client.prepare(req) + if err != nil { + return nil, err + } + for attempt := attempts.Start(); attempt.Next(); { + resp, err := b.Client.run(req, nil) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return nil, err + } + return resp, nil + } + panic("unreachable") +} + +// Exists checks whether or not an object exists on an bucket using a HEAD request. +func (b *Bucket) Exists(path string) (exists bool, err error) { + req := &request{ + method: "HEAD", + bucket: b.Name, + path: path, + } + err = b.Client.prepare(req) + if err != nil { + return + } + for attempt := attempts.Start(); attempt.Next(); { + resp, err := b.Client.run(req, nil) + + if shouldRetry(err) && attempt.HasNext() { + continue + } + + if err != nil { + // We can treat a 403 or 404 as non existance + if e, ok := err.(*Error); ok && (e.StatusCode == 403 || e.StatusCode == 404) { + return false, nil + } + return false, err + } + + if resp.StatusCode/100 == 2 { + exists = true + } + if resp.Body != nil { + resp.Body.Close() + } + return exists, err + } + return false, fmt.Errorf("OSS Currently Unreachable") +} + +// Head HEADs an object in the bucket, returns the response with +func (b *Bucket) Head(path string, headers http.Header) (*http.Response, error) { + req := &request{ + method: "HEAD", + bucket: b.Name, + path: path, + headers: headers, + } + err := b.Client.prepare(req) + if err != nil { + return nil, err + } + + for attempt := attempts.Start(); attempt.Next(); { + resp, err := b.Client.run(req, nil) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return nil, err + } + return resp, err + } + return nil, fmt.Errorf("OSS Currently Unreachable") +} + +// Put inserts an object into the bucket. +func (b *Bucket) Put(path string, data []byte, contType string, perm ACL, options Options) error { + body := bytes.NewBuffer(data) + return b.PutReader(path, body, int64(len(data)), contType, perm, options) +} + +// PutCopy puts a copy of an object given by the key path into bucket b using b.Path as the target key +func (b *Bucket) PutCopy(path string, perm ACL, options CopyOptions, source string) (*CopyObjectResult, error) { + headers := make(http.Header) + + headers.Set("x-oss-acl", string(perm)) + headers.Set("x-oss-copy-source", source) + + options.addHeaders(headers) + req := &request{ + method: "PUT", + bucket: b.Name, + path: path, + headers: headers, + } + resp := &CopyObjectResult{} + err := b.Client.query(req, resp) + if err != nil { + return resp, err + } + return resp, nil +} + +// PutReader inserts an object into the bucket by consuming data +// from r until EOF. +func (b *Bucket) PutReader(path string, r io.Reader, length int64, contType string, perm ACL, options Options) error { + headers := make(http.Header) + headers.Set("Content-Length", strconv.FormatInt(length, 10)) + headers.Set("Content-Type", contType) + headers.Set("x-oss-acl", string(perm)) + + options.addHeaders(headers) + req := &request{ + method: "PUT", + bucket: b.Name, + path: path, + headers: headers, + payload: r, + } + return b.Client.query(req, nil) +} + +// PutFile creates/updates object with file +func (b *Bucket) PutFile(path string, file *os.File, perm ACL, options Options) error { + var contentType string + if dotPos := strings.LastIndex(file.Name(), "."); dotPos == -1 { + contentType = DefaultContentType + } else { + if mimeType := mime.TypeByExtension(file.Name()[dotPos:]); mimeType == "" { + contentType = DefaultContentType + } else { + contentType = mimeType + } + } + stats, err := file.Stat() + if err != nil { + log.Panicf("Unable to read file %s stats.", file.Name()) + return nil + } + + return b.PutReader(path, file, stats.Size(), contentType, perm, options) +} + +// addHeaders adds o's specified fields to headers +func (o Options) addHeaders(headers http.Header) { + if o.ServerSideEncryption { + headers.Set("x-oss-server-side-encryption", "AES256") + } + if len(o.ContentEncoding) != 0 { + headers.Set("Content-Encoding", o.ContentEncoding) + } + if len(o.CacheControl) != 0 { + headers.Set("Cache-Control", o.CacheControl) + } + if len(o.ContentMD5) != 0 { + headers.Set("Content-MD5", o.ContentMD5) + } + if len(o.ContentDisposition) != 0 { + headers.Set("Content-Disposition", o.ContentDisposition) + } + + for k, v := range o.Meta { + for _, mv := range v { + headers.Add("x-oss-meta-"+k, mv) + } + } +} + +// addHeaders adds o's specified fields to headers +func (o CopyOptions) addHeaders(headers http.Header) { + if len(o.MetadataDirective) != 0 { + headers.Set("x-oss-metadata-directive", o.MetadataDirective) + } + if len(o.CopySourceOptions) != 0 { + headers.Set("x-oss-copy-source-range", o.CopySourceOptions) + } + if o.Headers != nil { + for k, v := range o.Headers { + newSlice := make([]string, len(v)) + copy(newSlice, v) + headers[k] = newSlice + } + } +} + +func makeXMLBuffer(doc []byte) *bytes.Buffer { + buf := new(bytes.Buffer) + buf.WriteString(xml.Header) + buf.Write(doc) + return buf +} + +type IndexDocument struct { + Suffix string `xml:"Suffix"` +} + +type ErrorDocument struct { + Key string `xml:"Key"` +} + +type RoutingRule struct { + ConditionKeyPrefixEquals string `xml:"Condition>KeyPrefixEquals"` + RedirectReplaceKeyPrefixWith string `xml:"Redirect>ReplaceKeyPrefixWith,omitempty"` + RedirectReplaceKeyWith string `xml:"Redirect>ReplaceKeyWith,omitempty"` +} + +type RedirectAllRequestsTo struct { + HostName string `xml:"HostName"` + Protocol string `xml:"Protocol,omitempty"` +} + +type WebsiteConfiguration struct { + XMLName xml.Name `xml:"http://doc.oss-cn-hangzhou.aliyuncs.com WebsiteConfiguration"` + IndexDocument *IndexDocument `xml:"IndexDocument,omitempty"` + ErrorDocument *ErrorDocument `xml:"ErrorDocument,omitempty"` + RoutingRules *[]RoutingRule `xml:"RoutingRules>RoutingRule,omitempty"` + RedirectAllRequestsTo *RedirectAllRequestsTo `xml:"RedirectAllRequestsTo,omitempty"` +} + +// PutBucketWebsite configures a bucket as a website. +func (b *Bucket) PutBucketWebsite(configuration WebsiteConfiguration) error { + doc, err := xml.Marshal(configuration) + if err != nil { + return err + } + + buf := makeXMLBuffer(doc) + + return b.PutBucketSubresource("website", buf, int64(buf.Len())) +} + +func (b *Bucket) PutBucketSubresource(subresource string, r io.Reader, length int64) error { + headers := make(http.Header) + headers.Set("Content-Length", strconv.FormatInt(length, 10)) + + req := &request{ + path: "/", + method: "PUT", + bucket: b.Name, + headers: headers, + payload: r, + params: url.Values{subresource: {""}}, + } + + return b.Client.query(req, nil) +} + +// Del removes an object from the bucket. +func (b *Bucket) Del(path string) error { + req := &request{ + method: "DELETE", + bucket: b.Name, + path: path, + } + return b.Client.query(req, nil) +} + +type Delete struct { + Quiet bool `xml:"Quiet,omitempty"` + Objects []Object `xml:"Object"` +} + +type Object struct { + Key string `xml:"Key"` + VersionId string `xml:"VersionId,omitempty"` +} + +// DelMulti removes up to 1000 objects from the bucket. +func (b *Bucket) DelMulti(objects Delete) error { + doc, err := xml.Marshal(objects) + if err != nil { + return err + } + + buf := makeXMLBuffer(doc) + digest := md5.New() + size, err := digest.Write(buf.Bytes()) + if err != nil { + return err + } + + headers := make(http.Header) + headers.Set("Content-Length", strconv.FormatInt(int64(size), 10)) + headers.Set("Content-MD5", base64.StdEncoding.EncodeToString(digest.Sum(nil))) + headers.Set("Content-Type", "text/xml") + + req := &request{ + path: "/", + method: "POST", + params: url.Values{"delete": {""}}, + bucket: b.Name, + headers: headers, + payload: buf, + } + + return b.Client.query(req, nil) +} + +// The ListResp type holds the results of a List bucket operation. +type ListResp struct { + Name string + Prefix string + Delimiter string + Marker string + MaxKeys int + // IsTruncated is true if the results have been truncated because + // there are more keys and prefixes than can fit in MaxKeys. + // N.B. this is the opposite sense to that documented (incorrectly) in + // http://goo.gl/YjQTc + IsTruncated bool + Contents []Key + CommonPrefixes []string `xml:">Prefix"` + // if IsTruncated is true, pass NextMarker as marker argument to List() + // to get the next set of keys + NextMarker string +} + +// The Key type represents an item stored in an bucket. +type Key struct { + Key string + LastModified string + Type string + Size int64 + // ETag gives the hex-encoded MD5 sum of the contents, + // surrounded with double-quotes. + ETag string + StorageClass string + Owner Owner +} + +// List returns information about objects in an bucket. +// +// The prefix parameter limits the response to keys that begin with the +// specified prefix. +// +// The delim parameter causes the response to group all of the keys that +// share a common prefix up to the next delimiter in a single entry within +// the CommonPrefixes field. You can use delimiters to separate a bucket +// into different groupings of keys, similar to how folders would work. +// +// The marker parameter specifies the key to start with when listing objects +// in a bucket. OSS lists objects in alphabetical order and +// will return keys alphabetically greater than the marker. +// +// The max parameter specifies how many keys + common prefixes to return in +// the response. The default is 1000. +// +// For example, given these keys in a bucket: +// +// index.html +// index2.html +// photos/2006/January/sample.jpg +// photos/2006/February/sample2.jpg +// photos/2006/February/sample3.jpg +// photos/2006/February/sample4.jpg +// +// Listing this bucket with delimiter set to "/" would yield the +// following result: +// +// &ListResp{ +// Name: "sample-bucket", +// MaxKeys: 1000, +// Delimiter: "/", +// Contents: []Key{ +// {Key: "index.html", "index2.html"}, +// }, +// CommonPrefixes: []string{ +// "photos/", +// }, +// } +// +// Listing the same bucket with delimiter set to "/" and prefix set to +// "photos/2006/" would yield the following result: +// +// &ListResp{ +// Name: "sample-bucket", +// MaxKeys: 1000, +// Delimiter: "/", +// Prefix: "photos/2006/", +// CommonPrefixes: []string{ +// "photos/2006/February/", +// "photos/2006/January/", +// }, +// } +// +func (b *Bucket) List(prefix, delim, marker string, max int) (result *ListResp, err error) { + params := make(url.Values) + params.Set("prefix", prefix) + params.Set("delimiter", delim) + params.Set("marker", marker) + if max != 0 { + params.Set("max-keys", strconv.FormatInt(int64(max), 10)) + } + req := &request{ + bucket: b.Name, + params: params, + } + result = &ListResp{} + for attempt := attempts.Start(); attempt.Next(); { + err = b.Client.query(req, result) + if !shouldRetry(err) { + break + } + } + if err != nil { + return nil, err + } + // if NextMarker is not returned, it should be set to the name of last key, + // so let's do it so that each caller doesn't have to + if result.IsTruncated && result.NextMarker == "" { + n := len(result.Contents) + if n > 0 { + result.NextMarker = result.Contents[n-1].Key + } + } + return result, nil +} + +//// The VersionsResp type holds the results of a list bucket Versions operation. +//type VersionsResp struct { +// Name string +// Prefix string +// KeyMarker string +// VersionIdMarker string +// MaxKeys int +// Delimiter string +// IsTruncated bool +// Versions []Version `xml:"Version"` +// CommonPrefixes []string `xml:">Prefix"` +//} + +//// The Version type represents an object version stored in an bucket. +//type Version struct { +// Key string +// VersionId string +// IsLatest bool +// LastModified string +// // ETag gives the hex-encoded MD5 sum of the contents, +// // surrounded with double-quotes. +// ETag string +// Size int64 +// Owner Owner +// StorageClass string +//} + +//func (b *Bucket) Versions(prefix, delim, keyMarker string, versionIdMarker string, max int) (result *VersionsResp, err error) { +// params := url.Values{} +// params.Set("versions", "") +// params.Set("prefix", prefix) +// params.Set("delimiter", delim) + +// if len(versionIdMarker) != 0 { +// params["version-id-marker"] = []string{versionIdMarker} +// } +// if len(keyMarker) != 0 { +// params["key-marker"] = []string{keyMarker} +// } + +// if max != 0 { +// params["max-keys"] = []string{strconv.FormatInt(int64(max), 10)} +// } +// req := &request{ +// bucket: b.Name, +// params: params, +// } +// result = &VersionsResp{} +// for attempt := attempts.Start(); attempt.Next(); { +// err = b.Client.query(req, result) +// if !shouldRetry(err) { +// break +// } +// } +// if err != nil { +// return nil, err +// } +// return result, nil +//} + +type GetLocationResp struct { + Location string `xml:",innerxml"` +} + +func (b *Bucket) Location() (string, error) { + params := make(url.Values) + params.Set("location", "") + r, err := b.GetWithParams("/", params) + + if err != nil { + return "", err + } + + // Parse the XML response. + var resp GetLocationResp + if err = xml.Unmarshal(r, &resp); err != nil { + return "", err + } + + if resp.Location == "" { + return string(Hangzhou), nil + } + return resp.Location, nil +} + +func (b *Bucket) Path(path string) string { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return "/" + b.Name + path +} + +// URL returns a non-signed URL that allows retriving the +// object at path. It only works if the object is publicly +// readable (see SignedURL). +func (b *Bucket) URL(path string) string { + req := &request{ + bucket: b.Name, + path: path, + } + err := b.Client.prepare(req) + if err != nil { + panic(err) + } + u, err := req.url() + if err != nil { + panic(err) + } + u.RawQuery = "" + return u.String() +} + +// SignedURL returns a signed URL that allows anyone holding the URL +// to retrieve the object at path. The signature is valid until expires. +func (b *Bucket) SignedURL(path string, expires time.Time) string { + return b.SignedURLWithArgs(path, expires, nil, nil) +} + +// SignedURLWithArgs returns a signed URL that allows anyone holding the URL +// to retrieve the object at path. The signature is valid until expires. +func (b *Bucket) SignedURLWithArgs(path string, expires time.Time, params url.Values, headers http.Header) string { + return b.SignedURLWithMethod("GET", path, expires, params, headers) +} + +// SignedURLWithMethod returns a signed URL that allows anyone holding the URL +// to either retrieve the object at path or make a HEAD request against it. The signature is valid until expires. +func (b *Bucket) SignedURLWithMethod(method, path string, expires time.Time, params url.Values, headers http.Header) string { + var uv = url.Values{} + + if params != nil { + uv = params + } + + uv.Set("Expires", strconv.FormatInt(expires.Unix(), 10)) + uv.Set("OSSAccessKeyId", b.AccessKeyId) + + req := &request{ + method: method, + bucket: b.Name, + path: path, + params: uv, + headers: headers, + } + err := b.Client.prepare(req) + if err != nil { + panic(err) + } + u, err := req.url() + if err != nil { + panic(err) + } + + return u.String() +} + +// UploadSignedURL returns a signed URL that allows anyone holding the URL +// to upload the object at path. The signature is valid until expires. +// contenttype is a string like image/png +// name is the resource name in OSS terminology like images/ali.png [obviously excluding the bucket name itself] +func (b *Bucket) UploadSignedURL(name, method, contentType string, expires time.Time) string { + //TODO TESTING + expireDate := expires.Unix() + if method != "POST" { + method = "PUT" + } + + tokenData := "" + + stringToSign := method + "\n\n" + contentType + "\n" + strconv.FormatInt(expireDate, 10) + "\n" + tokenData + "/" + path.Join(b.Name, name) + secretKey := b.AccessKeySecret + accessId := b.AccessKeyId + mac := hmac.New(sha1.New, []byte(secretKey)) + mac.Write([]byte(stringToSign)) + macsum := mac.Sum(nil) + signature := base64.StdEncoding.EncodeToString([]byte(macsum)) + signature = strings.TrimSpace(signature) + + signedurl, err := url.Parse("https://" + b.Name + ".client.amazonaws.com/") + if err != nil { + log.Println("ERROR sining url for OSS upload", err) + return "" + } + signedurl.Path = name + params := url.Values{} + params.Add("OSSAccessKeyId", accessId) + params.Add("Expires", strconv.FormatInt(expireDate, 10)) + params.Add("Signature", signature) + + signedurl.RawQuery = params.Encode() + return signedurl.String() +} + +// PostFormArgsEx returns the action and input fields needed to allow anonymous +// uploads to a bucket within the expiration limit +// Additional conditions can be specified with conds +func (b *Bucket) PostFormArgsEx(path string, expires time.Time, redirect string, conds []string) (action string, fields map[string]string) { + conditions := []string{} + fields = map[string]string{ + "AWSAccessKeyId": b.AccessKeyId, + "key": path, + } + + if conds != nil { + conditions = append(conditions, conds...) + } + + conditions = append(conditions, fmt.Sprintf("{\"key\": \"%s\"}", path)) + conditions = append(conditions, fmt.Sprintf("{\"bucket\": \"%s\"}", b.Name)) + if redirect != "" { + conditions = append(conditions, fmt.Sprintf("{\"success_action_redirect\": \"%s\"}", redirect)) + fields["success_action_redirect"] = redirect + } + + vExpiration := expires.Format("2006-01-02T15:04:05Z") + vConditions := strings.Join(conditions, ",") + policy := fmt.Sprintf("{\"expiration\": \"%s\", \"conditions\": [%s]}", vExpiration, vConditions) + policy64 := base64.StdEncoding.EncodeToString([]byte(policy)) + fields["policy"] = policy64 + + signer := hmac.New(sha1.New, []byte(b.AccessKeySecret)) + signer.Write([]byte(policy64)) + fields["signature"] = base64.StdEncoding.EncodeToString(signer.Sum(nil)) + + action = fmt.Sprintf("%s/%s/", b.Client.Region, b.Name) + return +} + +// PostFormArgs returns the action and input fields needed to allow anonymous +// uploads to a bucket within the expiration limit +func (b *Bucket) PostFormArgs(path string, expires time.Time, redirect string) (action string, fields map[string]string) { + return b.PostFormArgsEx(path, expires, redirect, nil) +} + +type request struct { + method string + bucket string + path string + params url.Values + headers http.Header + baseurl string + payload io.Reader + prepared bool +} + +func (req *request) url() (*url.URL, error) { + u, err := url.Parse(req.baseurl) + if err != nil { + return nil, fmt.Errorf("bad OSS endpoint URL %q: %v", req.baseurl, err) + } + u.RawQuery = req.params.Encode() + u.Path = req.path + return u, nil +} + +// query prepares and runs the req request. +// If resp is not nil, the XML data contained in the response +// body will be unmarshalled on it. +func (client *Client) query(req *request, resp interface{}) error { + err := client.prepare(req) + if err != nil { + return err + } + r, err := client.run(req, resp) + if r != nil && r.Body != nil { + r.Body.Close() + } + return err +} + +// Sets baseurl on req from bucket name and the region endpoint +func (client *Client) setBaseURL(req *request) error { + req.baseurl = client.Region.GetEndpoint(client.Internal, req.bucket, client.Secure) + + return nil +} + +// partiallyEscapedPath partially escapes the OSS path allowing for all OSS REST API calls. +// +// Some commands including: +// GET Bucket acl http://goo.gl/aoXflF +// GET Bucket cors http://goo.gl/UlmBdx +// GET Bucket lifecycle http://goo.gl/8Fme7M +// GET Bucket policy http://goo.gl/ClXIo3 +// GET Bucket location http://goo.gl/5lh8RD +// GET Bucket Logging http://goo.gl/sZ5ckF +// GET Bucket notification http://goo.gl/qSSZKD +// GET Bucket tagging http://goo.gl/QRvxnM +// require the first character after the bucket name in the path to be a literal '?' and +// not the escaped hex representation '%3F'. +func partiallyEscapedPath(path string) string { + pathEscapedAndSplit := strings.Split((&url.URL{Path: path}).String(), "/") + if len(pathEscapedAndSplit) >= 3 { + if len(pathEscapedAndSplit[2]) >= 3 { + // Check for the one "?" that should not be escaped. + if pathEscapedAndSplit[2][0:3] == "%3F" { + pathEscapedAndSplit[2] = "?" + pathEscapedAndSplit[2][3:] + } + } + } + return strings.Replace(strings.Join(pathEscapedAndSplit, "/"), "+", "%2B", -1) +} + +// prepare sets up req to be delivered to OSS. +func (client *Client) prepare(req *request) error { + // Copy so they can be mutated without affecting on retries. + headers := copyHeader(req.headers) + params := make(url.Values) + + for k, v := range req.params { + params[k] = v + } + + req.params = params + req.headers = headers + + if !req.prepared { + req.prepared = true + if req.method == "" { + req.method = "GET" + } + + if !strings.HasPrefix(req.path, "/") { + req.path = "/" + req.path + } + + err := client.setBaseURL(req) + if err != nil { + return err + } + } + + req.headers.Set("Date", util.GetGMTime()) + client.signRequest(req) + + return nil +} + +// Prepares an *http.Request for doHttpRequest +func (client *Client) setupHttpRequest(req *request) (*http.Request, error) { + // Copy so that signing the http request will not mutate it + + u, err := req.url() + if err != nil { + return nil, err + } + u.Opaque = fmt.Sprintf("//%s%s", u.Host, partiallyEscapedPath(u.Path)) + + hreq := http.Request{ + URL: u, + Method: req.method, + ProtoMajor: 1, + ProtoMinor: 1, + Close: true, + Header: req.headers, + Form: req.params, + } + + contentLength := req.headers.Get("Content-Length") + + if contentLength != "" { + hreq.ContentLength, _ = strconv.ParseInt(contentLength, 10, 64) + req.headers.Del("Content-Length") + } + + if req.payload != nil { + hreq.Body = ioutil.NopCloser(req.payload) + } + + return &hreq, nil +} + +// doHttpRequest sends hreq and returns the http response from the server. +// If resp is not nil, the XML data contained in the response +// body will be unmarshalled on it. +func (client *Client) doHttpRequest(hreq *http.Request, resp interface{}) (*http.Response, error) { + c := http.Client{ + Transport: &http.Transport{ + Dial: func(netw, addr string) (c net.Conn, err error) { + deadline := time.Now().Add(client.ReadTimeout) + if client.ConnectTimeout > 0 { + c, err = net.DialTimeout(netw, addr, client.ConnectTimeout) + } else { + c, err = net.Dial(netw, addr) + } + if err != nil { + return + } + if client.ReadTimeout > 0 { + err = c.SetDeadline(deadline) + } + return + }, + Proxy: http.ProxyFromEnvironment, + }, + } + + hresp, err := c.Do(hreq) + if err != nil { + return nil, err + } + if client.debug { + log.Printf("%s %s %d\n", hreq.Method, hreq.URL.String(), hresp.StatusCode) + contentType := hresp.Header.Get("Content-Type") + if contentType == "application/xml" || contentType == "text/xml" { + dump, _ := httputil.DumpResponse(hresp, true) + log.Printf("} -> %s\n", dump) + } else { + log.Printf("Response Content-Type: %s\n", contentType) + } + } + if hresp.StatusCode != 200 && hresp.StatusCode != 204 && hresp.StatusCode != 206 { + return nil, client.buildError(hresp) + } + if resp != nil { + err = xml.NewDecoder(hresp.Body).Decode(resp) + hresp.Body.Close() + + if client.debug { + log.Printf("aliyungo.oss> decoded xml into %#v", resp) + } + + } + return hresp, err +} + +// run sends req and returns the http response from the server. +// If resp is not nil, the XML data contained in the response +// body will be unmarshalled on it. +func (client *Client) run(req *request, resp interface{}) (*http.Response, error) { + if client.debug { + log.Printf("Running OSS request: %#v", req) + } + + hreq, err := client.setupHttpRequest(req) + if err != nil { + return nil, err + } + + return client.doHttpRequest(hreq, resp) +} + +// Error represents an error in an operation with OSS. +type Error struct { + StatusCode int // HTTP status code (200, 403, ...) + Code string // OSS error code ("UnsupportedOperation", ...) + Message string // The human-oriented error message + BucketName string + RequestId string + HostId string +} + +func (e *Error) Error() string { + return e.Message +} + +func (client *Client) buildError(r *http.Response) error { + if client.debug { + log.Printf("got error (status code %v)", r.StatusCode) + data, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("\tread error: %v", err) + } else { + log.Printf("\tdata:\n%s\n\n", data) + } + r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) + } + + err := Error{} + // TODO return error if Unmarshal fails? + xml.NewDecoder(r.Body).Decode(&err) + r.Body.Close() + err.StatusCode = r.StatusCode + if err.Message == "" { + err.Message = r.Status + } + if client.debug { + log.Printf("err: %#v\n", err) + } + return &err +} + +func shouldRetry(err error) bool { + if err == nil { + return false + } + switch err { + case io.ErrUnexpectedEOF, io.EOF: + return true + } + switch e := err.(type) { + case *net.DNSError: + return true + case *net.OpError: + switch e.Op { + case "read", "write": + return true + } + case *url.Error: + // url.Error can be returned either by net/url if a URL cannot be + // parsed, or by net/http if the response is closed before the headers + // are received or parsed correctly. In that later case, e.Op is set to + // the HTTP method name with the first letter uppercased. We don't want + // to retry on POST operations, since those are not idempotent, all the + // other ones should be safe to retry. + switch e.Op { + case "Get", "Put", "Delete", "Head": + return shouldRetry(e.Err) + default: + return false + } + case *Error: + switch e.Code { + case "InternalError", "NoSuchUpload", "NoSuchBucket": + return true + } + } + return false +} + +func hasCode(err error, code string) bool { + e, ok := err.(*Error) + return ok && e.Code == code +} + +func copyHeader(header http.Header) (newHeader http.Header) { + newHeader = make(http.Header) + for k, v := range header { + newSlice := make([]string, len(v)) + copy(newSlice, v) + newHeader[k] = newSlice + } + return +} + +type AccessControlPolicy struct { + Owner Owner + Grants []string `xml:"AccessControlList>Grant"` +} + +// ACL returns ACL of bucket +func (b *Bucket) ACL() (result *AccessControlPolicy, err error) { + + params := make(url.Values) + params.Set("acl", "") + + r, err := b.GetWithParams("/", params) + if err != nil { + return nil, err + } + + // Parse the XML response. + var resp AccessControlPolicy + if err = xml.Unmarshal(r, &resp); err != nil { + return nil, err + } + + return &resp, nil +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/client_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/client_test.go new file mode 100644 index 00000000..13bc8768 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/client_test.go @@ -0,0 +1,211 @@ +package oss_test + +import ( + "bytes" + "io/ioutil" + //"net/http" + "testing" + "time" + + "github.com/denverdino/aliyungo/oss" +) + +var ( + //If you test on ECS, you can set the internal param to true + client = oss.NewOSSClient(TestRegion, false, TestAccessKeyId, TestAccessKeySecret, false) +) + +func TestCreateBucket(t *testing.T) { + + b := client.Bucket(TestBucket) + err := b.PutBucket(oss.Private) + if err != nil { + t.Errorf("Failed for PutBucket: %v", err) + } + t.Log("Wait a while for bucket creation ...") + time.Sleep(10 * time.Second) +} + +func TestHead(t *testing.T) { + + b := client.Bucket(TestBucket) + _, err := b.Head("name", nil) + + if err == nil { + t.Errorf("Failed for Head: %v", err) + } +} + +func TestPutObject(t *testing.T) { + const DISPOSITION = "attachment; filename=\"0x1a2b3c.jpg\"" + + b := client.Bucket(TestBucket) + err := b.Put("name", []byte("content"), "content-type", oss.Private, oss.Options{ContentDisposition: DISPOSITION}) + if err != nil { + t.Errorf("Failed for Put: %v", err) + } +} + +func TestGet(t *testing.T) { + + b := client.Bucket(TestBucket) + data, err := b.Get("name") + + if err != nil || string(data) != "content" { + t.Errorf("Failed for Get: %v", err) + } +} + +func TestURL(t *testing.T) { + + b := client.Bucket(TestBucket) + url := b.URL("name") + + t.Log("URL: ", url) + // /c.Assert(req.URL.Path, check.Equals, "/denverdino_test/name") +} + +func TestGetReader(t *testing.T) { + + b := client.Bucket(TestBucket) + rc, err := b.GetReader("name") + if err != nil { + t.Fatalf("Failed for GetReader: %v", err) + } + data, err := ioutil.ReadAll(rc) + rc.Close() + if err != nil || string(data) != "content" { + t.Errorf("Failed for ReadAll: %v", err) + } +} + +func aTestGetNotFound(t *testing.T) { + + b := client.Bucket("non-existent-bucket") + _, err := b.Get("non-existent") + if err == nil { + t.Fatalf("Failed for TestGetNotFound: %v", err) + } + ossErr, _ := err.(*oss.Error) + if ossErr.StatusCode != 404 || ossErr.BucketName != "non-existent-bucket" { + t.Errorf("Failed for TestGetNotFound: %v", err) + } + +} + +func TestPutCopy(t *testing.T) { + b := client.Bucket(TestBucket) + t.Log("Source: ", b.Path("name")) + res, err := b.PutCopy("newname", oss.Private, oss.CopyOptions{}, + b.Path("name")) + if err == nil { + t.Logf("Copy result: %v", res) + } else { + t.Errorf("Failed for PutCopy: %v", err) + } +} + +func TestList(t *testing.T) { + + b := client.Bucket(TestBucket) + + data, err := b.List("n", "", "", 0) + if err != nil || len(data.Contents) != 2 { + t.Errorf("Failed for List: %v", err) + } else { + t.Logf("Contents = %++v", data) + } +} + +func TestListWithDelimiter(t *testing.T) { + + b := client.Bucket(TestBucket) + + data, err := b.List("photos/2006/", "/", "some-marker", 1000) + if err != nil || len(data.Contents) != 0 { + t.Errorf("Failed for List: %v", err) + } else { + t.Logf("Contents = %++v", data) + } + +} + +func TestPutReader(t *testing.T) { + + b := client.Bucket(TestBucket) + buf := bytes.NewBufferString("content") + err := b.PutReader("name", buf, int64(buf.Len()), "content-type", oss.Private, oss.Options{}) + if err != nil { + t.Errorf("Failed for PutReader: %v", err) + } + TestGetReader(t) +} + +func TestExists(t *testing.T) { + + b := client.Bucket(TestBucket) + result, err := b.Exists("name") + if err != nil || result != true { + t.Errorf("Failed for Exists: %v", err) + } +} + +func TestLocation(t *testing.T) { + b := client.Bucket(TestBucket) + result, err := b.Location() + + if err != nil || result != string(TestRegion) { + t.Errorf("Failed for Location: %v %s", err, result) + } +} + +func TestACL(t *testing.T) { + b := client.Bucket(TestBucket) + result, err := b.ACL() + + if err != nil { + t.Errorf("Failed for ACL: %v", err) + } else { + t.Logf("AccessControlPolicy: %++v", result) + } +} + +func TestDelObject(t *testing.T) { + + b := client.Bucket(TestBucket) + err := b.Del("name") + if err != nil { + t.Errorf("Failed for Del: %v", err) + } +} + +func TestDelMultiObjects(t *testing.T) { + + b := client.Bucket(TestBucket) + objects := []oss.Object{oss.Object{Key: "newname"}} + err := b.DelMulti(oss.Delete{ + Quiet: false, + Objects: objects, + }) + if err != nil { + t.Errorf("Failed for DelMulti: %v", err) + } +} + +func TestGetService(t *testing.T) { + bucketList, err := client.GetService() + if err != nil { + t.Errorf("Unable to get service: %v", err) + } else { + t.Logf("GetService: %++v", bucketList) + } +} + +func TestDelBucket(t *testing.T) { + + b := client.Bucket(TestBucket) + err := b.DelBucket() + if err != nil { + t.Errorf("Failed for DelBucket: %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/config_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/config_test.go new file mode 100644 index 00000000..7c0d2549 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/config_test.go @@ -0,0 +1,14 @@ +package oss_test + +import ( + "github.com/denverdino/aliyungo/oss" +) + +//Modify with your Access Key Id and Access Key Secret +const ( + TestAccessKeyId = "MY_ACCESS_KEY_ID" + TestAccessKeySecret = "MY_ACCESS_KEY_ID" + TestIAmRich = false + TestRegion = oss.Beijing + TestBucket = "denverdino" +) diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/export.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/export.go new file mode 100644 index 00000000..ebdb0477 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/export.go @@ -0,0 +1,23 @@ +package oss + +import ( + "github.com/denverdino/aliyungo/util" +) + +var originalStrategy = attempts + +func SetAttemptStrategy(s *util.AttemptStrategy) { + if s == nil { + attempts = originalStrategy + } else { + attempts = *s + } +} + +func SetListPartsMax(n int) { + listPartsMax = n +} + +func SetListMultiMax(n int) { + listMultiMax = n +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/multi.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/multi.go new file mode 100644 index 00000000..454d53b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/multi.go @@ -0,0 +1,460 @@ +package oss + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "errors" + "io" + //"log" + "net/http" + "net/url" + "sort" + "strconv" + "strings" +) + +// Multi represents an unfinished multipart upload. +// +// Multipart uploads allow sending big objects in smaller chunks. +// After all parts have been sent, the upload must be explicitly +// completed by calling Complete with the list of parts. + +type Multi struct { + Bucket *Bucket + Key string + UploadId string +} + +// That's the default. Here just for testing. +var listMultiMax = 1000 + +type listMultiResp struct { + NextKeyMarker string + NextUploadIdMarker string + IsTruncated bool + Upload []Multi + CommonPrefixes []string `xml:"CommonPrefixes>Prefix"` +} + +// ListMulti returns the list of unfinished multipart uploads in b. +// +// The prefix parameter limits the response to keys that begin with the +// specified prefix. You can use prefixes to separate a bucket into different +// groupings of keys (to get the feeling of folders, for example). +// +// The delim parameter causes the response to group all of the keys that +// share a common prefix up to the next delimiter in a single entry within +// the CommonPrefixes field. You can use delimiters to separate a bucket +// into different groupings of keys, similar to how folders would work. +// +func (b *Bucket) ListMulti(prefix, delim string) (multis []*Multi, prefixes []string, err error) { + params := make(url.Values) + params.Set("uploads", "") + params.Set("max-uploads", strconv.FormatInt(int64(listMultiMax), 10)) + params.Set("prefix", prefix) + params.Set("delimiter", delim) + + for attempt := attempts.Start(); attempt.Next(); { + req := &request{ + method: "GET", + bucket: b.Name, + params: params, + } + var resp listMultiResp + err := b.Client.query(req, &resp) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return nil, nil, err + } + for i := range resp.Upload { + multi := &resp.Upload[i] + multi.Bucket = b + multis = append(multis, multi) + } + prefixes = append(prefixes, resp.CommonPrefixes...) + if !resp.IsTruncated { + return multis, prefixes, nil + } + params.Set("key-marker", resp.NextKeyMarker) + params.Set("upload-id-marker", resp.NextUploadIdMarker) + attempt = attempts.Start() // Last request worked. + } + panic("unreachable") +} + +// Multi returns a multipart upload handler for the provided key +// inside b. If a multipart upload exists for key, it is returned, +// otherwise a new multipart upload is initiated with contType and perm. +func (b *Bucket) Multi(key, contType string, perm ACL, options Options) (*Multi, error) { + multis, _, err := b.ListMulti(key, "") + if err != nil && !hasCode(err, "NoSuchUpload") { + return nil, err + } + for _, m := range multis { + if m.Key == key { + return m, nil + } + } + return b.InitMulti(key, contType, perm, options) +} + +// InitMulti initializes a new multipart upload at the provided +// key inside b and returns a value for manipulating it. +// +func (b *Bucket) InitMulti(key string, contType string, perm ACL, options Options) (*Multi, error) { + headers := make(http.Header) + headers.Set("Content-Length", "0") + headers.Set("Content-Type", contType) + headers.Set("x-oss-acl", string(perm)) + + options.addHeaders(headers) + params := make(url.Values) + params.Set("uploads", "") + req := &request{ + method: "POST", + bucket: b.Name, + path: key, + headers: headers, + params: params, + } + var err error + var resp struct { + UploadId string `xml:"UploadId"` + } + for attempt := attempts.Start(); attempt.Next(); { + err = b.Client.query(req, &resp) + if !shouldRetry(err) { + break + } + } + if err != nil { + return nil, err + } + return &Multi{Bucket: b, Key: key, UploadId: resp.UploadId}, nil +} + +func (m *Multi) PutPartCopy(n int, options CopyOptions, source string) (*CopyObjectResult, Part, error) { + headers := make(http.Header) + headers.Set("x-oss-copy-source", source) + + options.addHeaders(headers) + params := make(url.Values) + params.Set("uploadId", m.UploadId) + params.Set("partNumber", strconv.FormatInt(int64(n), 10)) + + sourceBucket := m.Bucket.Client.Bucket(strings.TrimRight(strings.Split(source, "/")[1], "/")) + //log.Println("source: ", source) + //log.Println("sourceBucket: ", sourceBucket.Name) + //log.Println("HEAD: ", strings.Split(source, "/")[2]) + sourceMeta, err := sourceBucket.Head(strings.Split(source, "/")[2], nil) + if err != nil { + return nil, Part{}, err + } + + for attempt := attempts.Start(); attempt.Next(); { + req := &request{ + method: "PUT", + bucket: m.Bucket.Name, + path: m.Key, + headers: headers, + params: params, + } + resp := &CopyObjectResult{} + err = m.Bucket.Client.query(req, resp) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return nil, Part{}, err + } + if resp.ETag == "" { + return nil, Part{}, errors.New("part upload succeeded with no ETag") + } + return resp, Part{n, resp.ETag, sourceMeta.ContentLength}, nil + } + panic("unreachable") +} + +// PutPart sends part n of the multipart upload, reading all the content from r. +// Each part, except for the last one, must be at least 5MB in size. +// +func (m *Multi) PutPart(n int, r io.ReadSeeker) (Part, error) { + partSize, _, md5b64, err := seekerInfo(r) + if err != nil { + return Part{}, err + } + return m.putPart(n, r, partSize, md5b64) +} + +func (m *Multi) putPart(n int, r io.ReadSeeker, partSize int64, md5b64 string) (Part, error) { + headers := make(http.Header) + headers.Set("Content-Length", strconv.FormatInt(partSize, 10)) + headers.Set("Content-MD5", md5b64) + + params := make(url.Values) + params.Set("uploadId", m.UploadId) + params.Set("partNumber", strconv.FormatInt(int64(n), 10)) + + for attempt := attempts.Start(); attempt.Next(); { + _, err := r.Seek(0, 0) + if err != nil { + return Part{}, err + } + req := &request{ + method: "PUT", + bucket: m.Bucket.Name, + path: m.Key, + headers: headers, + params: params, + payload: r, + } + err = m.Bucket.Client.prepare(req) + if err != nil { + return Part{}, err + } + resp, err := m.Bucket.Client.run(req, nil) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return Part{}, err + } + etag := resp.Header.Get("ETag") + if etag == "" { + return Part{}, errors.New("part upload succeeded with no ETag") + } + return Part{n, etag, partSize}, nil + } + panic("unreachable") +} + +func seekerInfo(r io.ReadSeeker) (size int64, md5hex string, md5b64 string, err error) { + _, err = r.Seek(0, 0) + if err != nil { + return 0, "", "", err + } + digest := md5.New() + size, err = io.Copy(digest, r) + if err != nil { + return 0, "", "", err + } + sum := digest.Sum(nil) + md5hex = hex.EncodeToString(sum) + md5b64 = base64.StdEncoding.EncodeToString(sum) + return size, md5hex, md5b64, nil +} + +type Part struct { + N int `xml:"PartNumber"` + ETag string + Size int64 +} + +type partSlice []Part + +func (s partSlice) Len() int { return len(s) } +func (s partSlice) Less(i, j int) bool { return s[i].N < s[j].N } +func (s partSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type listPartsResp struct { + NextPartNumberMarker string + IsTruncated bool + Part []Part +} + +// That's the default. Here just for testing. +var listPartsMax = 1000 + +// ListParts for backcompatability. See the documentation for ListPartsFull +func (m *Multi) ListParts() ([]Part, error) { + return m.ListPartsFull(0, listPartsMax) +} + +// ListPartsFull returns the list of previously uploaded parts in m, +// ordered by part number (Only parts with higher part numbers than +// partNumberMarker will be listed). Only up to maxParts parts will be +// returned. +// +func (m *Multi) ListPartsFull(partNumberMarker int, maxParts int) ([]Part, error) { + if maxParts > listPartsMax { + maxParts = listPartsMax + } + + params := make(url.Values) + params.Set("uploadId", m.UploadId) + params.Set("max-parts", strconv.FormatInt(int64(maxParts), 10)) + params.Set("part-number-marker", strconv.FormatInt(int64(partNumberMarker), 10)) + + var parts partSlice + for attempt := attempts.Start(); attempt.Next(); { + req := &request{ + method: "GET", + bucket: m.Bucket.Name, + path: m.Key, + params: params, + } + var resp listPartsResp + err := m.Bucket.Client.query(req, &resp) + if shouldRetry(err) && attempt.HasNext() { + continue + } + if err != nil { + return nil, err + } + parts = append(parts, resp.Part...) + if !resp.IsTruncated { + sort.Sort(parts) + return parts, nil + } + params.Set("part-number-marker", resp.NextPartNumberMarker) + attempt = attempts.Start() // Last request worked. + } + panic("unreachable") +} + +type ReaderAtSeeker interface { + io.ReaderAt + io.ReadSeeker +} + +// PutAll sends all of r via a multipart upload with parts no larger +// than partSize bytes, which must be set to at least 5MB. +// Parts previously uploaded are either reused if their checksum +// and size match the new part, or otherwise overwritten with the +// new content. +// PutAll returns all the parts of m (reused or not). +func (m *Multi) PutAll(r ReaderAtSeeker, partSize int64) ([]Part, error) { + old, err := m.ListParts() + if err != nil && !hasCode(err, "NoSuchUpload") { + return nil, err + } + reuse := 0 // Index of next old part to consider reusing. + current := 1 // Part number of latest good part handled. + totalSize, err := r.Seek(0, 2) + if err != nil { + return nil, err + } + first := true // Must send at least one empty part if the file is empty. + var result []Part +NextSection: + for offset := int64(0); offset < totalSize || first; offset += partSize { + first = false + if offset+partSize > totalSize { + partSize = totalSize - offset + } + section := io.NewSectionReader(r, offset, partSize) + _, md5hex, md5b64, err := seekerInfo(section) + if err != nil { + return nil, err + } + for reuse < len(old) && old[reuse].N <= current { + // Looks like this part was already sent. + part := &old[reuse] + etag := `"` + md5hex + `"` + if part.N == current && part.Size == partSize && part.ETag == etag { + // Checksum matches. Reuse the old part. + result = append(result, *part) + current++ + continue NextSection + } + reuse++ + } + + // Part wasn't found or doesn't match. Send it. + part, err := m.putPart(current, section, partSize, md5b64) + if err != nil { + return nil, err + } + result = append(result, part) + current++ + } + return result, nil +} + +type completeUpload struct { + XMLName xml.Name `xml:"CompleteMultipartUpload"` + Parts completeParts `xml:"Part"` +} + +type completePart struct { + PartNumber int + ETag string +} + +type completeParts []completePart + +func (p completeParts) Len() int { return len(p) } +func (p completeParts) Less(i, j int) bool { return p[i].PartNumber < p[j].PartNumber } +func (p completeParts) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// Complete assembles the given previously uploaded parts into the +// final object. This operation may take several minutes. +// +func (m *Multi) Complete(parts []Part) error { + params := make(url.Values) + params.Set("uploadId", m.UploadId) + + c := completeUpload{} + for _, p := range parts { + c.Parts = append(c.Parts, completePart{p.N, p.ETag}) + } + sort.Sort(c.Parts) + data, err := xml.Marshal(&c) + if err != nil { + return err + } + for attempt := attempts.Start(); attempt.Next(); { + req := &request{ + method: "POST", + bucket: m.Bucket.Name, + path: m.Key, + params: params, + payload: bytes.NewReader(data), + } + err := m.Bucket.Client.query(req, nil) + if shouldRetry(err) && attempt.HasNext() { + continue + } + return err + } + panic("unreachable") +} + +// Abort deletes an unifinished multipart upload and any previously +// uploaded parts for it. +// +// After a multipart upload is aborted, no additional parts can be +// uploaded using it. However, if any part uploads are currently in +// progress, those part uploads might or might not succeed. As a result, +// it might be necessary to abort a given multipart upload multiple +// times in order to completely free all storage consumed by all parts. +// +// NOTE: If the described scenario happens to you, please report back to +// the goamz authors with details. In the future such retrying should be +// handled internally, but it's not clear what happens precisely (Is an +// error returned? Is the issue completely undetectable?). +// +func (m *Multi) Abort() error { + params := make(url.Values) + params.Set("uploadId", m.UploadId) + + for attempt := attempts.Start(); attempt.Next(); { + req := &request{ + method: "DELETE", + bucket: m.Bucket.Name, + path: m.Key, + params: params, + } + err := m.Bucket.Client.query(req, nil) + if shouldRetry(err) && attempt.HasNext() { + continue + } + return err + } + panic("unreachable") +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/multi_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/multi_test.go new file mode 100644 index 00000000..6ecd63be --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/multi_test.go @@ -0,0 +1,161 @@ +package oss_test + +import ( + //"encoding/xml" + "github.com/denverdino/aliyungo/oss" + "testing" + //"io" + //"io/ioutil" + "strings" +) + +func TestCreateBucketMulti(t *testing.T) { + TestCreateBucket(t) +} + +func TestInitMulti(t *testing.T) { + b := client.Bucket(TestBucket) + + metadata := make(map[string][]string) + metadata["key1"] = []string{"value1"} + metadata["key2"] = []string{"value2"} + options := oss.Options{ + ServerSideEncryption: true, + Meta: metadata, + ContentEncoding: "text/utf8", + CacheControl: "no-cache", + ContentMD5: "0000000000000000", + } + + multi, err := b.InitMulti("multi", "text/plain", oss.Private, options) + if err != nil { + t.Errorf("Failed for InitMulti: %v", err) + } else { + t.Logf("InitMulti result: %++v", multi) + } +} + +func TestMultiReturnOld(t *testing.T) { + + b := client.Bucket(TestBucket) + + multi, err := b.Multi("multi", "text/plain", oss.Private, oss.Options{}) + if err != nil { + t.Errorf("Failed for Multi: %v", err) + } else { + t.Logf("Multi result: %++v", multi) + } + +} + +func TestPutPart(t *testing.T) { + + b := client.Bucket(TestBucket) + + multi, err := b.Multi("multi", "text/plain", oss.Private, oss.Options{}) + if err != nil { + t.Fatalf("Failed for Multi: %v", err) + } + + part, err := multi.PutPart(1, strings.NewReader("")) + if err != nil { + t.Errorf("Failed for PutPart: %v", err) + } else { + t.Logf("PutPart result: %++v", part) + } + +} +func TestPutPartCopy(t *testing.T) { + + TestPutObject(t) + + b := client.Bucket(TestBucket) + + multi, err := b.Multi("multi", "text/plain", oss.Private, oss.Options{}) + if err != nil { + t.Fatalf("Failed for Multi: %v", err) + } + + res, part, err := multi.PutPartCopy(2, oss.CopyOptions{}, b.Path("name")) + if err != nil { + t.Errorf("Failed for PutPartCopy: %v", err) + } else { + t.Logf("PutPartCopy result: %++v %++v", part, res) + } + TestDelObject(t) +} + +func TestListParts(t *testing.T) { + + b := client.Bucket(TestBucket) + + multi, err := b.Multi("multi", "text/plain", oss.Private, oss.Options{}) + if err != nil { + t.Fatalf("Failed for Multi: %v", err) + } + + parts, err := multi.ListParts() + if err != nil { + t.Errorf("Failed for ListParts: %v", err) + } else { + t.Logf("ListParts result: %++v", parts) + } +} +func TestListMulti(t *testing.T) { + + b := client.Bucket(TestBucket) + + multis, prefixes, err := b.ListMulti("", "/") + if err != nil { + t.Errorf("Failed for ListMulti: %v", err) + } else { + t.Logf("ListMulti result : %++v %++v", multis, prefixes) + } +} +func TestMultiAbort(t *testing.T) { + + b := client.Bucket(TestBucket) + + multi, err := b.Multi("multi", "text/plain", oss.Private, oss.Options{}) + if err != nil { + t.Fatalf("Failed for Multi: %v", err) + } + + err = multi.Abort() + if err != nil { + t.Errorf("Failed for Abort: %v", err) + } + +} + +func TestPutAll(t *testing.T) { + TestInitMulti(t) + // Don't retry the NoSuchUpload error. + b := client.Bucket(TestBucket) + + multi, err := b.Multi("multi", "text/plain", oss.Private, oss.Options{}) + if err != nil { + t.Fatalf("Failed for Multi: %v", err) + } + + // Must send at least one part, so that completing it will work. + parts, err := multi.PutAll(strings.NewReader("part1part2last"), 5) + if err != nil { + t.Errorf("Failed for PutAll: %v", err) + } else { + t.Logf("PutAll result: %++v", parts) + } + // // Must send at least one part, so that completing it will work. + // err = multi.Complete(parts) + // if err != nil { + // t.Errorf("Failed for Complete: %v", err) + // } + err = multi.Abort() + if err != nil { + t.Errorf("Failed for Abort: %v", err) + } +} + +func TestCleanUp(t *testing.T) { + TestDelBucket(t) +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/regions.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/regions.go new file mode 100644 index 00000000..2bba7382 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/regions.go @@ -0,0 +1,53 @@ +package oss + +import ( + "fmt" +) + +// Region represents OSS region +type Region string + +// Constants of region definition +const ( + Hangzhou = Region("oss-cn-hangzhou") + Qingdao = Region("oss-cn-qingdao") + Beijing = Region("oss-cn-beijing") + Hongkong = Region("oss-cn-hongkong") + Shenzhen = Region("oss-cn-shenzhen") + USWest1 = Region("oss-us-west-1") + DefaultRegion = Hangzhou +) + +// GetEndpoint returns endpoint of region +func (r Region) GetEndpoint(internal bool, bucket string, secure bool) string { + if internal { + return r.GetInternalEndpoint(bucket, secure) + } + return r.GetInternetEndpoint(bucket, secure) +} + +func getProtocol(secure bool) string { + protocol := "http" + if secure { + protocol = "https" + } + return protocol +} + +// GetInternetEndpoint returns internet endpoint of region +func (r Region) GetInternetEndpoint(bucket string, secure bool) string { + protocol := getProtocol(secure) + if bucket == "" { + return fmt.Sprintf("%s://oss.aliyuncs.com", protocol) + } + return fmt.Sprintf("%s://%s.%s.aliyuncs.com", protocol, bucket, string(r)) +} + +// GetInternalEndpoint returns internal endpoint of region +func (r Region) GetInternalEndpoint(bucket string, secure bool) string { + protocol := getProtocol(secure) + if bucket == "" { + return fmt.Sprintf("%s://oss-internal.aliyuncs.com", protocol) + } + return fmt.Sprintf("%s://%s.%s-internal.aliyuncs.com", protocol, bucket, string(r)) +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/signature.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/signature.go new file mode 100644 index 00000000..a261644a --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/oss/signature.go @@ -0,0 +1,105 @@ +package oss + +import ( + "github.com/denverdino/aliyungo/util" + //"log" + "net/http" + "net/url" + "sort" + "strings" +) + +const HeaderOSSPrefix = "x-oss-" + +var ossParamsToSign = map[string]bool{ + "acl": true, + "delete": true, + "location": true, + "logging": true, + "notification": true, + "partNumber": true, + "policy": true, + "requestPayment": true, + "torrent": true, + "uploadId": true, + "uploads": true, + "versionId": true, + "versioning": true, + "versions": true, + "response-content-type": true, + "response-content-language": true, + "response-expires": true, + "response-cache-control": true, + "response-content-disposition": true, + "response-content-encoding": true, +} + +func (client *Client) signRequest(request *request) { + query := request.params + + urlSignature := query.Get("OSSAccessKeyId") != "" + + headers := request.headers + contentMd5 := headers.Get("Content-Md5") + contentType := headers.Get("Content-Type") + date := "" + if urlSignature { + date = query.Get("Expires") + } else { + date = headers.Get("Date") + } + + resource := request.path + if request.bucket != "" { + resource = "/" + request.bucket + request.path + } + params := make(url.Values) + for k, v := range query { + if ossParamsToSign[k] { + params[k] = v + } + } + + if len(params) > 0 { + resource = resource + "?" + util.Encode(params) + } + + canonicalizedResource := resource + + _, canonicalizedHeader := canonicalizeHeader(headers) + + stringToSign := request.method + "\n" + contentMd5 + "\n" + contentType + "\n" + date + "\n" + canonicalizedHeader + canonicalizedResource + + //log.Println("stringToSign: ", stringToSign) + signature := util.CreateSignature(stringToSign, client.AccessKeySecret) + + if query.Get("OSSAccessKeyId") != "" { + query.Set("Signature", signature) + } else { + headers.Set("Authorization", "OSS "+client.AccessKeyId+":"+signature) + } +} + +//Have to break the abstraction to append keys with lower case. +func canonicalizeHeader(headers http.Header) (newHeaders http.Header, result string) { + var canonicalizedHeaders []string + newHeaders = http.Header{} + + for k, v := range headers { + if lower := strings.ToLower(k); strings.HasPrefix(lower, HeaderOSSPrefix) { + newHeaders[lower] = v + canonicalizedHeaders = append(canonicalizedHeaders, lower) + } else { + newHeaders[k] = v + } + } + + sort.Strings(canonicalizedHeaders) + + var canonicalizedHeader string + + for _, k := range canonicalizedHeaders { + canonicalizedHeader += k + ":" + headers.Get(k) + "\n" + } + return newHeaders, canonicalizedHeader +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/attempt.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/attempt.go new file mode 100644 index 00000000..e71ed19f --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/attempt.go @@ -0,0 +1,74 @@ +package util + +import ( + "time" +) + +// AttemptStrategy represents a strategy for waiting for an action +// to complete successfully. This is an internal type used by the +// implementation of other goamz packages. +type AttemptStrategy struct { + Total time.Duration // total duration of attempt. + Delay time.Duration // interval between each try in the burst. + Min int // minimum number of retries; overrides Total +} + +type Attempt struct { + strategy AttemptStrategy + last time.Time + end time.Time + force bool + count int +} + +// Start begins a new sequence of attempts for the given strategy. +func (s AttemptStrategy) Start() *Attempt { + now := time.Now() + return &Attempt{ + strategy: s, + last: now, + end: now.Add(s.Total), + force: true, + } +} + +// Next waits until it is time to perform the next attempt or returns +// false if it is time to stop trying. +func (a *Attempt) Next() bool { + now := time.Now() + sleep := a.nextSleep(now) + if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count { + return false + } + a.force = false + if sleep > 0 && a.count > 0 { + time.Sleep(sleep) + now = time.Now() + } + a.count++ + a.last = now + return true +} + +func (a *Attempt) nextSleep(now time.Time) time.Duration { + sleep := a.strategy.Delay - now.Sub(a.last) + if sleep < 0 { + return 0 + } + return sleep +} + +// HasNext returns whether another attempt will be made if the current +// one fails. If it returns true, the following call to Next is +// guaranteed to return true. +func (a *Attempt) HasNext() bool { + if a.force || a.strategy.Min > a.count { + return true + } + now := time.Now() + if now.Add(a.nextSleep(now)).Before(a.end) { + a.force = true + return true + } + return false +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/attempt_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/attempt_test.go new file mode 100644 index 00000000..50e9be7a --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/attempt_test.go @@ -0,0 +1,90 @@ +package util + +import ( + "testing" + "time" +) + +func TestAttemptTiming(t *testing.T) { + testAttempt := AttemptStrategy{ + Total: 0.25e9, + Delay: 0.1e9, + } + want := []time.Duration{0, 0.1e9, 0.2e9, 0.2e9} + got := make([]time.Duration, 0, len(want)) // avoid allocation when testing timing + t0 := time.Now() + for a := testAttempt.Start(); a.Next(); { + got = append(got, time.Now().Sub(t0)) + } + got = append(got, time.Now().Sub(t0)) + if len(got) != len(want) { + t.Fatalf("Failed!") + } + const margin = 0.01e9 + for i, got := range want { + lo := want[i] - margin + hi := want[i] + margin + if got < lo || got > hi { + t.Errorf("attempt %d want %g got %g", i, want[i].Seconds(), got.Seconds()) + } + } +} + +func TestAttemptNextHasNext(t *testing.T) { + a := AttemptStrategy{}.Start() + if !a.Next() { + t.Fatalf("Failed!") + } + if a.Next() { + t.Fatalf("Failed!") + } + + a = AttemptStrategy{}.Start() + if !a.Next() { + t.Fatalf("Failed!") + } + if a.HasNext() { + t.Fatalf("Failed!") + } + if a.Next() { + t.Fatalf("Failed!") + } + a = AttemptStrategy{Total: 2e8}.Start() + + if !a.Next() { + t.Fatalf("Failed!") + } + if !a.HasNext() { + t.Fatalf("Failed!") + } + time.Sleep(2e8) + + if !a.HasNext() { + t.Fatalf("Failed!") + } + if !a.Next() { + t.Fatalf("Failed!") + } + if a.Next() { + t.Fatalf("Failed!") + } + + a = AttemptStrategy{Total: 1e8, Min: 2}.Start() + time.Sleep(1e8) + + if !a.Next() { + t.Fatalf("Failed!") + } + if !a.HasNext() { + t.Fatalf("Failed!") + } + if !a.Next() { + t.Fatalf("Failed!") + } + if a.HasNext() { + t.Fatalf("Failed!") + } + if a.Next() { + t.Fatalf("Failed!") + } +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/encoding.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/encoding.go new file mode 100644 index 00000000..54a63569 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/encoding.go @@ -0,0 +1,113 @@ +package util + +import ( + "encoding/json" + "fmt" + "log" + "net/url" + "reflect" + "strconv" + "time" +) + +//ConvertToQueryValues converts the struct to url.Values +func ConvertToQueryValues(ifc interface{}) url.Values { + values := url.Values{} + SetQueryValues(ifc, &values) + return values +} + +//SetQueryValues sets the struct to existing url.Values following ECS encoding rules +func SetQueryValues(ifc interface{}, values *url.Values) { + setQueryValues(ifc, values, "") +} + +func setQueryValues(i interface{}, values *url.Values, prefix string) { + elem := reflect.ValueOf(i) + if elem.Kind() == reflect.Ptr { + elem = elem.Elem() + } + elemType := elem.Type() + for i := 0; i < elem.NumField(); i++ { + fieldName := elemType.Field(i).Name + field := elem.Field(i) + // TODO Use Tag for validation + // tag := typ.Field(i).Tag.Get("tagname") + kind := field.Kind() + if (kind == reflect.Ptr || kind == reflect.Array || kind == reflect.Slice || kind == reflect.Map || kind == reflect.Chan) && field.IsNil() { + continue + } + if kind == reflect.Ptr { + field = field.Elem() + } + var value string + switch field.Interface().(type) { + case int, int8, int16, int32, int64: + i := field.Int() + if i != 0 { + value = strconv.FormatInt(i, 10) + } + case uint, uint8, uint16, uint32, uint64: + i := field.Uint() + if i != 0 { + value = strconv.FormatUint(i, 10) + } + case float32: + value = strconv.FormatFloat(field.Float(), 'f', 4, 32) + case float64: + value = strconv.FormatFloat(field.Float(), 'f', 4, 64) + case []byte: + value = string(field.Bytes()) + case bool: + value = strconv.FormatBool(field.Bool()) + case string: + value = field.String() + case []string: + l := field.Len() + if l > 0 { + strArray := make([]string, l) + for i := 0; i < l; i++ { + strArray[i] = field.Index(i).String() + } + bytes, err := json.Marshal(strArray) + if err == nil { + value = string(bytes) + } else { + log.Printf("Failed to convert JSON: %v", err) + } + } + case time.Time: + t := field.Interface().(time.Time) + value = GetISO8601TimeStamp(t) + + default: + if kind == reflect.Slice { //Array of structs + l := field.Len() + for j := 0; j < l; j++ { + prefixName := fmt.Sprintf("%s.%d.", fieldName, (j + 1)) + ifc := field.Index(j).Interface() + log.Printf("%s : %v", prefixName, ifc) + if ifc != nil { + setQueryValues(ifc, values, prefixName) + } + } + } else { + ifc := field.Interface() + if ifc != nil { + SetQueryValues(ifc, values) + continue + } + } + } + if value != "" { + name := elemType.Field(i).Tag.Get("ArgName") + if name == "" { + name = fieldName + } + if prefix != "" { + name = prefix + name + } + values.Set(name, value) + } + } +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/encoding_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/encoding_test.go new file mode 100644 index 00000000..a49eb215 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/encoding_test.go @@ -0,0 +1,46 @@ +package util + +import ( + "testing" + "time" +) + +type SubStruct struct { + A string + B int +} + +type TestStruct struct { + Format string + Version string + AccessKeyId string + Timestamp time.Time + Empty string + IntValue int `ArgName:"int-value"` + BoolPtr *bool `ArgName:"bool-ptr"` + IntPtr *int `ArgName:"int-ptr"` + StringArray []string `ArgName:"str-array"` + StructArray []SubStruct +} + +func TestConvertToQueryValues(t *testing.T) { + boolValue := true + request := TestStruct{ + Format: "JSON", + Version: "1.0", + Timestamp: time.Date(2015, time.Month(5), 26, 1, 2, 3, 4, time.UTC), + IntValue: 10, + BoolPtr: &boolValue, + StringArray: []string{"abc", "xyz"}, + StructArray: []SubStruct{ + SubStruct{A: "a", B: 1}, + SubStruct{A: "x", B: 2}, + }, + } + result := ConvertToQueryValues(&request).Encode() + const expectedResult = "Format=JSON&StructArray.1.A=a&StructArray.1.B=1&StructArray.2.A=x&StructArray.2.B=2&Timestamp=2015-05-26T01%3A02%3A03Z&Version=1.0&bool-ptr=true&int-value=10&str-array=%5B%22abc%22%2C%22xyz%22%5D" + if result != expectedResult { + t.Error("Incorrect encoding: ", result) + } + +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/iso6801.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/iso6801.go new file mode 100644 index 00000000..121d6e62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/iso6801.go @@ -0,0 +1,62 @@ +package util + +import ( + "fmt" + "time" +) + +// GetISO8601TimeStamp gets timestamp string in ISO8601 format +func GetISO8601TimeStamp(ts time.Time) string { + t := ts.UTC() + return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) +} + +const formatISO8601 = "2006-01-02T15:04:05Z" +const jsonFormatISO8601 = `"` + formatISO8601 + `"` + +// A ISO6801Time represents a time in ISO8601 format +type ISO6801Time time.Time + +// New constructs a new iso8601.Time instance from an existing +// time.Time instance. This causes the nanosecond field to be set to +// 0, and its time zone set to a fixed zone with no offset from UTC +// (but it is *not* UTC itself). +func New(t time.Time) ISO6801Time { + return ISO6801Time(time.Date( + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + t.Second(), + 0, + time.UTC, + )) +} + +// IsDefault checks if the time is default +func (it *ISO6801Time) IsDefault() bool { + return *it == ISO6801Time{} +} + +// MarshalJSON serializes the ISO6801Time into JSON string +func (it ISO6801Time) MarshalJSON() ([]byte, error) { + return []byte(time.Time(it).Format(jsonFormatISO8601)), nil +} + +// UnmarshalJSON deserializes the ISO6801Time from JSON string +func (it *ISO6801Time) UnmarshalJSON(data []byte) error { + if string(data) == "\"\"" { + return nil + } + t, err := time.ParseInLocation(jsonFormatISO8601, string(data), time.UTC) + if err == nil { + *it = ISO6801Time(t) + } + return err +} + +// String returns the time in ISO6801Time format +func (it ISO6801Time) String() string { + return time.Time(it).String() +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/iso6801_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/iso6801_test.go new file mode 100644 index 00000000..284a23c1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/iso6801_test.go @@ -0,0 +1,50 @@ +package util + +import ( + "encoding/json" + "testing" + "time" +) + +func TestISO8601Time(t *testing.T) { + now := New(time.Now().UTC()) + + data, err := json.Marshal(now) + if err != nil { + t.Fatal(err) + } + + _, err = time.Parse(`"`+formatISO8601+`"`, string(data)) + if err != nil { + t.Fatal(err) + } + + var now2 ISO6801Time + err = json.Unmarshal(data, &now2) + if err != nil { + t.Fatal(err) + } + + if now != now2 { + t.Fatalf("Time %s does not equal expected %s", now2, now) + } + + if now.String() != now2.String() { + t.Fatalf("String format for %s does not equal expected %s", now2, now) + } + + type TestTimeStruct struct { + A int + B *ISO6801Time + } + var testValue TestTimeStruct + err = json.Unmarshal([]byte("{\"A\": 1, \"B\":\"\"}"), &testValue) + if err != nil { + t.Fatal(err) + } + t.Logf("%v", testValue) + if !testValue.B.IsDefault() { + t.Fatal("Invaid Unmarshal result for ISO6801Time from empty value") + } + +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/signature.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/signature.go new file mode 100644 index 00000000..a00b27c1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/signature.go @@ -0,0 +1,40 @@ +package util + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "net/url" + "strings" +) + +//CreateSignature creates signature for string following Aliyun rules +func CreateSignature(stringToSignature, accessKeySecret string) string { + // Crypto by HMAC-SHA1 + hmacSha1 := hmac.New(sha1.New, []byte(accessKeySecret)) + hmacSha1.Write([]byte(stringToSignature)) + sign := hmacSha1.Sum(nil) + + // Encode to Base64 + base64Sign := base64.StdEncoding.EncodeToString(sign) + + return base64Sign +} + +func percentReplace(str string) string { + str = strings.Replace(str, "+", "%20", -1) + str = strings.Replace(str, "*", "%2A", -1) + str = strings.Replace(str, "%7E", "~", -1) + + return str +} + +// CreateSignatureForRequest creates signature for query string values +func CreateSignatureForRequest(method string, values *url.Values, accessKeySecret string) string { + + canonicalizedQueryString := percentReplace(values.Encode()) + + stringToSign := method + "&%2F&" + url.QueryEscape(canonicalizedQueryString) + + return CreateSignature(stringToSign, accessKeySecret) +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/signature_test.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/signature_test.go new file mode 100644 index 00000000..e5c22cca --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/signature_test.go @@ -0,0 +1,14 @@ +package util + +import ( + "testing" +) + +func TestCreateSignature(t *testing.T) { + + str := "GET&%2F&AccessKeyId%3Dtestid%26Action%3DDescribeRegions%26Format%3DXML%26RegionId%3Dregion1%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3DNwDAxvLU6tFE0DVb%26SignatureVersion%3D1.0%26TimeStamp%3D2012-12-26T10%253A33%253A56Z%26Version%3D2014-05-26" + + signature := CreateSignature(str, "testsecret") + + t.Log(signature) +} diff --git a/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/util.go b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/util.go new file mode 100644 index 00000000..f2826684 --- /dev/null +++ b/Godeps/_workspace/src/github.com/denverdino/aliyungo/util/util.go @@ -0,0 +1,54 @@ +package util + +import ( + "bytes" + "math/rand" + "net/http" + "net/url" + "sort" + "strconv" + "time" +) + +//CreateRandomString create random string +func CreateRandomString() string { + + rand.Seed(time.Now().UnixNano()) + randInt := rand.Int63() + randStr := strconv.FormatInt(randInt, 36) + + return randStr +} + +// Encode encodes the values into ``URL encoded'' form +// ("acl&bar=baz&foo=quux") sorted by key. +func Encode(v url.Values) string { + if v == nil { + return "" + } + var buf bytes.Buffer + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vs := v[k] + prefix := url.QueryEscape(k) + for _, v := range vs { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(prefix) + if v != "" { + buf.WriteString("=") + buf.WriteString(url.QueryEscape(v)) + } + } + } + return buf.String() +} + +func GetGMTime() string { + return time.Now().UTC().Format(http.TimeFormat) +}