package oss import ( "bytes" "crypto/md5" "encoding/base64" "encoding/xml" "fmt" "hash" "hash/crc64" "io" "net/http" "net/url" "os" "strconv" "strings" "time" ) // Bucket implements the operations of object. type Bucket struct { Client Client BucketName string } // PutObject creates a new object and it will overwrite the original one if it exists already. // // objectKey the object key in UTF-8 encoding. The length must be between 1 and 1023, and cannot start with "/" or "\". // reader io.Reader instance for reading the data for uploading // options the options for uploading the object. The valid options here are CacheControl, ContentDisposition, ContentEncoding // Expires, ServerSideEncryption, ObjectACL and Meta. Refer to the link below for more details. // https://help.aliyun.com/document_detail/oss/api-reference/object/PutObject.html // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) PutObject(objectKey string, reader io.Reader, options ...Option) error { opts := addContentType(options, objectKey) request := &PutObjectRequest{ ObjectKey: objectKey, Reader: reader, } resp, err := bucket.DoPutObject(request, opts) if err != nil { return err } defer resp.Body.Close() return err } // PutObjectFromFile creates a new object from the local file. // // objectKey object key. // filePath the local file path to upload. // options the options for uploading the object. Refer to the parameter options in PutObject for more details. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) PutObjectFromFile(objectKey, filePath string, options ...Option) error { fd, err := os.Open(filePath) if err != nil { return err } defer fd.Close() opts := addContentType(options, filePath, objectKey) request := &PutObjectRequest{ ObjectKey: objectKey, Reader: fd, } resp, err := bucket.DoPutObject(request, opts) if err != nil { return err } defer resp.Body.Close() return err } // DoPutObject does the actual upload work. // // request the request instance for uploading an object. // options the options for uploading an object. // // Response the response from OSS. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DoPutObject(request *PutObjectRequest, options []Option) (*Response, error) { isOptSet, _, _ := isOptionSet(options, HTTPHeaderContentType) if !isOptSet { options = addContentType(options, request.ObjectKey) } listener := getProgressListener(options) params := map[string]interface{}{} resp, err := bucket.do("PUT", request.ObjectKey, params, options, request.Reader, listener) if err != nil { return nil, err } if bucket.getConfig().IsEnableCRC { err = checkCRC(resp, "DoPutObject") if err != nil { return resp, err } } err = checkRespCode(resp.StatusCode, []int{http.StatusOK}) return resp, err } // GetObject downloads the object. // // objectKey the object key. // options the options for downloading the object. The valid values are: Range, IfModifiedSince, IfUnmodifiedSince, IfMatch, // IfNoneMatch, AcceptEncoding. For more details, please check out: // https://help.aliyun.com/document_detail/oss/api-reference/object/GetObject.html // // io.ReadCloser reader instance for reading data from response. It must be called close() after the usage and only valid when error is nil. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) GetObject(objectKey string, options ...Option) (io.ReadCloser, error) { result, err := bucket.DoGetObject(&GetObjectRequest{objectKey}, options) if err != nil { return nil, err } return result.Response, nil } // GetObjectToFile downloads the data to a local file. // // objectKey the object key to download. // filePath the local file to store the object data. // options the options for downloading the object. Refer to the parameter options in method GetObject for more details. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) GetObjectToFile(objectKey, filePath string, options ...Option) error { tempFilePath := filePath + TempFileSuffix // Calls the API to actually download the object. Returns the result instance. result, err := bucket.DoGetObject(&GetObjectRequest{objectKey}, options) if err != nil { return err } defer result.Response.Close() // If the local file does not exist, create a new one. If it exists, overwrite it. fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, FilePermMode) if err != nil { return err } // Copy the data to the local file path. _, err = io.Copy(fd, result.Response.Body) fd.Close() if err != nil { return err } // Compares the CRC value hasRange, _, _ := isOptionSet(options, HTTPHeaderRange) encodeOpt, _ := findOption(options, HTTPHeaderAcceptEncoding, nil) acceptEncoding := "" if encodeOpt != nil { acceptEncoding = encodeOpt.(string) } if bucket.getConfig().IsEnableCRC && !hasRange && acceptEncoding != "gzip" { result.Response.ClientCRC = result.ClientCRC.Sum64() err = checkCRC(result.Response, "GetObjectToFile") if err != nil { os.Remove(tempFilePath) return err } } return os.Rename(tempFilePath, filePath) } // DoGetObject is the actual API that gets the object. It's the internal function called by other public APIs. // // request the request to download the object. // options the options for downloading the file. Checks out the parameter options in method GetObject. // // GetObjectResult the result instance of getting the object. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DoGetObject(request *GetObjectRequest, options []Option) (*GetObjectResult, error) { params, _ := getRawParams(options) resp, err := bucket.do("GET", request.ObjectKey, 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 = TeeReader(resp.Body, crcCalc, contentLen, listener, nil) return result, nil } // CopyObject copies the object inside the bucket. // // srcObjectKey the source object to copy. // destObjectKey the target object to copy. // options options for copying an object. You can specify the conditions of copy. The valid conditions are CopySourceIfMatch, // CopySourceIfNoneMatch, CopySourceIfModifiedSince, CopySourceIfUnmodifiedSince, MetadataDirective. // Also you can specify the target object's attributes, such as CacheControl, ContentDisposition, ContentEncoding, Expires, // ServerSideEncryption, ObjectACL, Meta. Refer to the link below for more details : // https://help.aliyun.com/document_detail/oss/api-reference/object/CopyObject.html // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) CopyObject(srcObjectKey, destObjectKey string, options ...Option) (CopyObjectResult, error) { var out CopyObjectResult //first find version id versionIdKey := "versionId" versionId, _ := findOption(options, versionIdKey, nil) if versionId == nil { options = append(options, CopySource(bucket.BucketName, url.QueryEscape(srcObjectKey))) } else { options = deleteOption(options, versionIdKey) options = append(options, CopySourceVersion(bucket.BucketName, url.QueryEscape(srcObjectKey), versionId.(string))) } params := map[string]interface{}{} resp, err := bucket.do("PUT", destObjectKey, params, options, nil, nil) if err != nil { return out, err } defer resp.Body.Close() err = xmlUnmarshal(resp.Body, &out) return out, err } // CopyObjectTo copies the object to another bucket. // // srcObjectKey source object key. The source bucket is Bucket.BucketName . // destBucketName target bucket name. // destObjectKey target object name. // options copy options, check out parameter options in function CopyObject for more details. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) CopyObjectTo(destBucketName, destObjectKey, srcObjectKey string, options ...Option) (CopyObjectResult, error) { return bucket.copy(srcObjectKey, destBucketName, destObjectKey, options...) } // // CopyObjectFrom copies the object to another bucket. // // srcBucketName source bucket name. // srcObjectKey source object name. // destObjectKey target object name. The target bucket name is Bucket.BucketName. // options copy options. Check out parameter options in function CopyObject. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) CopyObjectFrom(srcBucketName, srcObjectKey, destObjectKey string, options ...Option) (CopyObjectResult, error) { destBucketName := bucket.BucketName var out CopyObjectResult srcBucket, err := bucket.Client.Bucket(srcBucketName) if err != nil { return out, err } return srcBucket.copy(srcObjectKey, destBucketName, destObjectKey, options...) } func (bucket Bucket) copy(srcObjectKey, destBucketName, destObjectKey string, options ...Option) (CopyObjectResult, error) { var out CopyObjectResult //first find version id versionIdKey := "versionId" versionId, _ := findOption(options, versionIdKey, nil) if versionId == nil { options = append(options, CopySource(bucket.BucketName, url.QueryEscape(srcObjectKey))) } else { options = deleteOption(options, versionIdKey) options = append(options, CopySourceVersion(bucket.BucketName, url.QueryEscape(srcObjectKey), versionId.(string))) } headers := make(map[string]string) err := handleOptions(headers, options) if err != nil { return out, err } params := map[string]interface{}{} resp, err := bucket.Client.Conn.Do("PUT", destBucketName, destObjectKey, params, headers, nil, 0, nil) // get response header respHeader, _ := findOption(options, responseHeader, nil) if respHeader != nil { pRespHeader := respHeader.(*http.Header) *pRespHeader = resp.Headers } if err != nil { return out, err } defer resp.Body.Close() err = xmlUnmarshal(resp.Body, &out) return out, err } // AppendObject uploads the data in the way of appending an existing or new object. // // AppendObject the parameter appendPosition specifies which postion (in the target object) to append. For the first append (to a non-existing file), // the appendPosition should be 0. The appendPosition in the subsequent calls will be the current object length. // For example, the first appendObject's appendPosition is 0 and it uploaded 65536 bytes data, then the second call's position is 65536. // The response header x-oss-next-append-position after each successful request also specifies the next call's append position (so the caller need not to maintain this information). // // objectKey the target object to append to. // reader io.Reader. The read instance for reading the data to append. // appendPosition the start position to append. // destObjectProperties the options for the first appending, such as CacheControl, ContentDisposition, ContentEncoding, // Expires, ServerSideEncryption, ObjectACL. // // int64 the next append position, it's valid when error is nil. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) AppendObject(objectKey string, reader io.Reader, appendPosition int64, options ...Option) (int64, error) { request := &AppendObjectRequest{ ObjectKey: objectKey, Reader: reader, Position: appendPosition, } result, err := bucket.DoAppendObject(request, options) if err != nil { return appendPosition, err } return result.NextPosition, err } // DoAppendObject is the actual API that does the object append. // // request the request object for appending object. // options the options for appending object. // // AppendObjectResult the result object for appending object. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DoAppendObject(request *AppendObjectRequest, options []Option) (*AppendObjectResult, error) { params := map[string]interface{}{} params["append"] = nil params["position"] = strconv.FormatInt(request.Position, 10) headers := make(map[string]string) opts := addContentType(options, request.ObjectKey) handleOptions(headers, opts) var initCRC uint64 isCRCSet, initCRCOpt, _ := isOptionSet(options, initCRC64) if isCRCSet { initCRC = initCRCOpt.(uint64) } listener := getProgressListener(options) handleOptions(headers, opts) resp, err := bucket.Client.Conn.Do("POST", bucket.BucketName, request.ObjectKey, params, headers, request.Reader, initCRC, listener) // get response header respHeader, _ := findOption(options, responseHeader, nil) if respHeader != nil { pRespHeader := respHeader.(*http.Header) *pRespHeader = resp.Headers } if err != nil { return nil, err } defer resp.Body.Close() nextPosition, _ := strconv.ParseInt(resp.Headers.Get(HTTPHeaderOssNextAppendPosition), 10, 64) result := &AppendObjectResult{ NextPosition: nextPosition, CRC: resp.ServerCRC, } if bucket.getConfig().IsEnableCRC && isCRCSet { err = checkCRC(resp, "AppendObject") if err != nil { return result, err } } return result, nil } // DeleteObject deletes the object. // // objectKey the object key to delete. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DeleteObject(objectKey string, options ...Option) error { params, _ := getRawParams(options) resp, err := bucket.do("DELETE", objectKey, params, options, nil, nil) if err != nil { return err } defer resp.Body.Close() return checkRespCode(resp.StatusCode, []int{http.StatusNoContent}) } // DeleteObjects deletes multiple objects. // // objectKeys the object keys to delete. // options the options for deleting objects. // Supported option is DeleteObjectsQuiet which means it will not return error even deletion failed (not recommended). By default it's not used. // // DeleteObjectsResult the result object. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DeleteObjects(objectKeys []string, options ...Option) (DeleteObjectsResult, error) { out := DeleteObjectsResult{} dxml := deleteXML{} for _, key := range objectKeys { dxml.Objects = append(dxml.Objects, DeleteObject{Key: key}) } isQuiet, _ := findOption(options, deleteObjectsQuiet, false) dxml.Quiet = isQuiet.(bool) bs, err := xml.Marshal(dxml) if err != nil { return out, err } buffer := new(bytes.Buffer) buffer.Write(bs) contentType := http.DetectContentType(buffer.Bytes()) options = append(options, ContentType(contentType)) sum := md5.Sum(bs) b64 := base64.StdEncoding.EncodeToString(sum[:]) options = append(options, ContentMD5(b64)) 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 } defer resp.Body.Close() deletedResult := DeleteObjectVersionsResult{} if !dxml.Quiet { if err = xmlUnmarshal(resp.Body, &deletedResult); err == nil { err = decodeDeleteObjectsResult(&deletedResult) } } // Keep compatibility:need convert to struct DeleteObjectsResult out.XMLName = deletedResult.XMLName for _, v := range deletedResult.DeletedObjectsDetail { out.DeletedObjects = append(out.DeletedObjects, v.Key) } return out, err } // DeleteObjectVersions deletes multiple object versions. // // objectVersions the object keys and versions to delete. // options the options for deleting objects. // Supported option is DeleteObjectsQuiet which means it will not return error even deletion failed (not recommended). By default it's not used. // // DeleteObjectVersionsResult the result object. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DeleteObjectVersions(objectVersions []DeleteObject, options ...Option) (DeleteObjectVersionsResult, error) { out := DeleteObjectVersionsResult{} dxml := deleteXML{} dxml.Objects = objectVersions isQuiet, _ := findOption(options, deleteObjectsQuiet, false) dxml.Quiet = isQuiet.(bool) bs, err := xml.Marshal(dxml) if err != nil { return out, err } buffer := new(bytes.Buffer) buffer.Write(bs) contentType := http.DetectContentType(buffer.Bytes()) options = append(options, ContentType(contentType)) sum := md5.Sum(bs) b64 := base64.StdEncoding.EncodeToString(sum[:]) options = append(options, ContentMD5(b64)) 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 } defer resp.Body.Close() if !dxml.Quiet { if err = xmlUnmarshal(resp.Body, &out); err == nil { err = decodeDeleteObjectsResult(&out) } } return out, err } // IsObjectExist checks if the object exists. // // bool flag of object's existence (true:exists; false:non-exist) when error is nil. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) IsObjectExist(objectKey string, options ...Option) (bool, error) { _, err := bucket.GetObjectMeta(objectKey, options...) if err == nil { return true, nil } switch err.(type) { case ServiceError: if err.(ServiceError).StatusCode == 404 { return false, nil } } return false, err } // ListObjects lists the objects under the current bucket. // // options it contains all the filters for listing objects. // It could specify a prefix filter on object keys, the max keys count to return and the object key marker and the delimiter for grouping object names. // The key marker means the returned objects' key must be greater than it in lexicographic order. // // For example, if the bucket has 8 objects, my-object-1, my-object-11, my-object-2, my-object-21, // my-object-22, my-object-3, my-object-31, my-object-32. If the prefix is my-object-2 (no other filters), then it returns // my-object-2, my-object-21, my-object-22 three objects. If the marker is my-object-22 (no other filters), then it returns // my-object-3, my-object-31, my-object-32 three objects. If the max keys is 5, then it returns 5 objects. // The three filters could be used together to achieve filter and paging functionality. // If the prefix is the folder name, then it could list all files under this folder (including the files under its subfolders). // But if the delimiter is specified with '/', then it only returns that folder's files (no subfolder's files). The direct subfolders are in the commonPrefixes properties. // For example, if the bucket has three objects fun/test.jpg, fun/movie/001.avi, fun/movie/007.avi. And if the prefix is "fun/", then it returns all three objects. // But if the delimiter is '/', then only "fun/test.jpg" is returned as files and fun/movie/ is returned as common prefix. // // For common usage scenario, check out sample/list_object.go. // // ListObjectsResponse the return value after operation succeeds (only valid when error is nil). // func (bucket Bucket) ListObjects(options ...Option) (ListObjectsResult, error) { var out ListObjectsResult options = append(options, EncodingType("url")) params, err := getRawParams(options) if err != nil { return out, err } resp, err := bucket.do("GET", "", params, options, nil, nil) if err != nil { return out, err } defer resp.Body.Close() err = xmlUnmarshal(resp.Body, &out) if err != nil { return out, err } err = decodeListObjectsResult(&out) return out, err } // ListObjectVersions lists objects of all versions under the current bucket. func (bucket Bucket) ListObjectVersions(options ...Option) (ListObjectVersionsResult, error) { var out ListObjectVersionsResult options = append(options, EncodingType("url")) params, err := getRawParams(options) if err != nil { return out, err } params["versions"] = nil resp, err := bucket.do("GET", "", params, options, nil, nil) if err != nil { return out, err } defer resp.Body.Close() err = xmlUnmarshal(resp.Body, &out) if err != nil { return out, err } err = decodeListObjectVersionsResult(&out) return out, err } // SetObjectMeta sets the metadata of the Object. // // objectKey object // options options for setting the metadata. The valid options are CacheControl, ContentDisposition, ContentEncoding, Expires, // ServerSideEncryption, and custom metadata. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) SetObjectMeta(objectKey string, options ...Option) error { options = append(options, MetadataDirective(MetaReplace)) _, err := bucket.CopyObject(objectKey, objectKey, options...) return err } // GetObjectDetailedMeta gets the object's detailed metadata // // objectKey object key. // options the constraints of the object. Only when the object meets the requirements this method will return the metadata. Otherwise returns error. Valid options are IfModifiedSince, IfUnmodifiedSince, // IfMatch, IfNoneMatch. For more details check out https://help.aliyun.com/document_detail/oss/api-reference/object/HeadObject.html // // http.Header object meta when error is nil. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) GetObjectDetailedMeta(objectKey string, options ...Option) (http.Header, error) { params, _ := getRawParams(options) resp, err := bucket.do("HEAD", objectKey, params, options, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() return resp.Headers, nil } // GetObjectMeta gets object metadata. // // GetObjectMeta is more lightweight than GetObjectDetailedMeta as it only returns basic metadata including ETag // size, LastModified. The size information is in the HTTP header Content-Length. // // objectKey object key // // http.Header the object's metadata, valid when error is nil. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) GetObjectMeta(objectKey string, options ...Option) (http.Header, error) { params, _ := getRawParams(options) params["objectMeta"] = nil //resp, err := bucket.do("GET", objectKey, "?objectMeta", "", nil, nil, nil) resp, err := bucket.do("HEAD", objectKey, params, options, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() return resp.Headers, nil } // SetObjectACL updates the object's ACL. // // Only the bucket's owner could update object's ACL which priority is higher than bucket's ACL. // For example, if the bucket ACL is private and object's ACL is public-read-write. // Then object's ACL is used and it means all users could read or write that object. // When the object's ACL is not set, then bucket's ACL is used as the object's ACL. // // Object read operations include GetObject, HeadObject, CopyObject and UploadPartCopy on the source object; // Object write operations include PutObject, PostObject, AppendObject, DeleteObject, DeleteMultipleObjects, // CompleteMultipartUpload and CopyObject on target object. // // objectKey the target object key (to set the ACL on) // objectAcl object ACL. Valid options are PrivateACL, PublicReadACL, PublicReadWriteACL. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) SetObjectACL(objectKey string, objectACL ACLType, options ...Option) error { options = append(options, ObjectACL(objectACL)) params, _ := getRawParams(options) params["acl"] = nil resp, err := bucket.do("PUT", objectKey, params, options, nil, nil) if err != nil { return err } defer resp.Body.Close() return checkRespCode(resp.StatusCode, []int{http.StatusOK}) } // GetObjectACL gets object's ACL // // objectKey the object to get ACL from. // // GetObjectACLResult the result object when error is nil. GetObjectACLResult.Acl is the object ACL. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) GetObjectACL(objectKey string, options ...Option) (GetObjectACLResult, error) { var out GetObjectACLResult params, _ := getRawParams(options) params["acl"] = nil resp, err := bucket.do("GET", objectKey, params, options, nil, nil) if err != nil { return out, err } defer resp.Body.Close() err = xmlUnmarshal(resp.Body, &out) return out, err } // PutSymlink creates a symlink (to point to an existing object) // // Symlink cannot point to another symlink. // When creating a symlink, it does not check the existence of the target file, and does not check if the target file is symlink. // Neither it checks the caller's permission on the target file. All these checks are deferred to the actual GetObject call via this symlink. // If trying to add an existing file, as long as the caller has the write permission, the existing one will be overwritten. // If the x-oss-meta- is specified, it will be added as the metadata of the symlink file. // // symObjectKey the symlink object's key. // targetObjectKey the target object key to point to. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) PutSymlink(symObjectKey string, targetObjectKey string, options ...Option) error { options = append(options, symlinkTarget(url.QueryEscape(targetObjectKey))) params, _ := getRawParams(options) params["symlink"] = nil resp, err := bucket.do("PUT", symObjectKey, params, options, nil, nil) if err != nil { return err } defer resp.Body.Close() return checkRespCode(resp.StatusCode, []int{http.StatusOK}) } // GetSymlink gets the symlink object with the specified key. // If the symlink object does not exist, returns 404. // // objectKey the symlink object's key. // // error it's nil if no error, otherwise it's an error object. // When error is nil, the target file key is in the X-Oss-Symlink-Target header of the returned object. // func (bucket Bucket) GetSymlink(objectKey string, options ...Option) (http.Header, error) { params, _ := getRawParams(options) params["symlink"] = nil resp, err := bucket.do("GET", objectKey, params, options, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() targetObjectKey := resp.Headers.Get(HTTPHeaderOssSymlinkTarget) targetObjectKey, err = url.QueryUnescape(targetObjectKey) if err != nil { return resp.Headers, err } resp.Headers.Set(HTTPHeaderOssSymlinkTarget, targetObjectKey) return resp.Headers, err } // RestoreObject restores the object from the archive storage. // // An archive object is in cold status by default and it cannot be accessed. // When restore is called on the cold object, it will become available for access after some time. // If multiple restores are called on the same file when the object is being restored, server side does nothing for additional calls but returns success. // By default, the restored object is available for access for one day. After that it will be unavailable again. // But if another RestoreObject are called after the file is restored, then it will extend one day's access time of that object, up to 7 days. // // objectKey object key to restore. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) RestoreObject(objectKey string, options ...Option) error { params, _ := getRawParams(options) params["restore"] = nil resp, err := bucket.do("POST", objectKey, params, options, nil, nil) if err != nil { return err } defer resp.Body.Close() return checkRespCode(resp.StatusCode, []int{http.StatusOK, http.StatusAccepted}) } // SignURL signs the URL. Users could access the object directly with this URL without getting the AK. // // objectKey the target object to sign. // signURLConfig the configuration for the signed URL // // string returns the signed URL, when error is nil. // error it's nil if no error, otherwise it's an error object. // 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 uploads an object with the URL. If the object exists, it will be overwritten. // PutObjectWithURL It will not generate minetype according to the key name. // // signedURL signed URL. // reader io.Reader the read instance for reading the data for the upload. // options the options for uploading the data. The valid options are CacheControl, ContentDisposition, ContentEncoding, // Expires, ServerSideEncryption, ObjectACL and custom metadata. Check out the following link for details: // https://help.aliyun.com/document_detail/oss/api-reference/object/PutObject.html // // error it's nil if no error, otherwise it's an error object. // 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 uploads an object from a local file with the signed URL. // PutObjectFromFileWithURL It does not generate mimetype according to object key's name or the local file name. // // signedURL the signed URL. // filePath local file path, such as dirfile.txt, for uploading. // options options for uploading, same as the options in PutObject function. // // error it's nil if no error, otherwise it's an error object. // 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 is the actual API that does the upload with URL work(internal for SDK) // // signedURL the signed URL. // reader io.Reader the read instance for getting the data to upload. // options options for uploading. // // Response the response object which contains the HTTP response. // error it's nil if no error, otherwise it's an error object. // 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 downloads the object and returns the reader instance, with the signed URL. // // signedURL the signed URL. // options options for downloading the object. Valid options are IfModifiedSince, IfUnmodifiedSince, IfMatch, // IfNoneMatch, AcceptEncoding. For more information, check out the following link: // https://help.aliyun.com/document_detail/oss/api-reference/object/GetObject.html // // io.ReadCloser the reader object for getting the data from response. It needs be closed after the usage. It's only valid when error is nil. // error it's nil if no error, otherwise it's an error object. // 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, nil } // GetObjectToFileWithURL downloads the object into a local file with the signed URL. // // signedURL the signed URL // filePath the local file path to download to. // options the options for downloading object. Check out the parameter options in function GetObject for the reference. // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) GetObjectToFileWithURL(signedURL, filePath string, options ...Option) error { tempFilePath := filePath + TempFileSuffix // Get the object's content result, err := bucket.DoGetObjectWithURL(signedURL, options) if err != nil { return err } defer result.Response.Close() // If the file does not exist, create one. If exists, then overwrite it. fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, FilePermMode) if err != nil { return err } // Save the data to the file. _, err = io.Copy(fd, result.Response.Body) fd.Close() if err != nil { return err } // Compare the CRC value. If CRC values do not match, return error. hasRange, _, _ := isOptionSet(options, HTTPHeaderRange) encodeOpt, _ := findOption(options, HTTPHeaderAcceptEncoding, nil) acceptEncoding := "" if encodeOpt != nil { acceptEncoding = encodeOpt.(string) } if bucket.getConfig().IsEnableCRC && !hasRange && acceptEncoding != "gzip" { 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 is the actual API that downloads the file with the signed URL. // // signedURL the signed URL. // options the options for getting object. Check out parameter options in GetObject for the reference. // // GetObjectResult the result object when the error is nil. // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) DoGetObjectWithURL(signedURL string, options []Option) (*GetObjectResult, error) { params, _ := getRawParams(options) 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 = TeeReader(resp.Body, crcCalc, contentLen, listener, nil) return result, nil } // // ProcessObject apply process on the specified image file. // // The supported process includes resize, rotate, crop, watermark, format, // udf, customized style, etc. // // // objectKey object key to process. // process process string, such as "image/resize,w_100|sys/saveas,o_dGVzdC5qcGc,b_dGVzdA" // // error it's nil if no error, otherwise it's an error object. // func (bucket Bucket) ProcessObject(objectKey string, process string, options ...Option) (ProcessObjectResult, error) { var out ProcessObjectResult params, _ := getRawParams(options) params["x-oss-process"] = nil processData := fmt.Sprintf("%v=%v", "x-oss-process", process) data := strings.NewReader(processData) resp, err := bucket.do("POST", objectKey, params, nil, data, nil) if err != nil { return out, err } defer resp.Body.Close() err = jsonUnmarshal(resp.Body, &out) return out, err } // // PutObjectTagging add tagging to object // // objectKey object key to add tagging // tagging tagging to be added // // error nil if success, otherwise error // func (bucket Bucket) PutObjectTagging(objectKey string, tagging Tagging, options ...Option) error { bs, err := xml.Marshal(tagging) if err != nil { return err } buffer := new(bytes.Buffer) buffer.Write(bs) params, _ := getRawParams(options) params["tagging"] = nil resp, err := bucket.do("PUT", objectKey, params, options, buffer, nil) if err != nil { return err } defer resp.Body.Close() return nil } // // GetObjectTagging get tagging of the object // // objectKey object key to get tagging // // Tagging // error nil if success, otherwise error func (bucket Bucket) GetObjectTagging(objectKey string, options ...Option) (GetObjectTaggingResult, error) { var out GetObjectTaggingResult params, _ := getRawParams(options) params["tagging"] = nil resp, err := bucket.do("GET", objectKey, params, options, nil, nil) if err != nil { return out, err } defer resp.Body.Close() err = xmlUnmarshal(resp.Body, &out) return out, err } // // DeleteObjectTagging delete object taggging // // objectKey object key to delete tagging // // error nil if success, otherwise error // func (bucket Bucket) DeleteObjectTagging(objectKey string, options ...Option) error { params, _ := getRawParams(options) params["tagging"] = nil if objectKey == "" { return fmt.Errorf("invalid argument: object name is empty") } resp, err := bucket.do("DELETE", objectKey, params, options, nil, nil) if err != nil { return err } defer resp.Body.Close() return checkRespCode(resp.StatusCode, []int{http.StatusNoContent}) } func (bucket Bucket) OptionsMethod(objectKey string, options ...Option) (http.Header, error) { var out http.Header resp, err := bucket.do("OPTIONS", objectKey, nil, options, nil, nil) if err != nil { return out, err } defer resp.Body.Close() out = resp.Headers return out, nil } // Private 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) if err != nil { return nil, err } err = CheckBucketName(bucket.BucketName) if len(bucket.BucketName) > 0 && err != nil { return nil, err } resp, err := bucket.Client.Conn.Do(method, bucket.BucketName, objectName, params, headers, data, 0, listener) // get response header respHeader, _ := findOption(options, responseHeader, nil) if respHeader != nil { pRespHeader := respHeader.(*http.Header) *pRespHeader = resp.Headers } return resp, err } 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 } resp, err := bucket.Client.Conn.DoURL(method, signedURL, headers, data, 0, listener) // get response header respHeader, _ := findOption(options, responseHeader, nil) if respHeader != nil { pRespHeader := respHeader.(*http.Header) *pRespHeader = resp.Headers } return resp, err } func (bucket Bucket) getConfig() *Config { return bucket.Client.Config } func addContentType(options []Option, keys ...string) []Option { typ := TypeByExtension("") for _, key := range keys { typ = TypeByExtension(key) if typ != "" { break } } if typ == "" { typ = "application/octet-stream" } opts := []Option{ContentType(typ)} opts = append(opts, options...) return opts }