diff --git a/api-get-conditions.go b/api-get-conditions.go new file mode 100644 index 0000000000..bbeeba1290 --- /dev/null +++ b/api-get-conditions.go @@ -0,0 +1,82 @@ +/* + * Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2016 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package minio + +import ( + "fmt" + "net/http" + "time" +) + +// GetConditions - get conditions implement methods for setting +// conditions for a GetObject request. +// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html +type GetConditions struct { + http.Header +} + +// SetMatchETag - set match etag. +func (c GetConditions) SetMatchETag(etag string) error { + if etag == "" { + return ErrInvalidArgument("ETag cannot be empty.") + } + c.Set("If-Match", etag) + return nil +} + +// SetMatchETagExcept - set match etag except. +func (c GetConditions) SetMatchETagExcept(etag string) error { + if etag == "" { + return ErrInvalidArgument("ETag cannot be empty.") + } + c.Set("If-None-Match", etag) + return nil +} + +// SetUnmodified - set unmodified time since. +func (c GetConditions) SetUnmodified(modTime time.Time) error { + if modTime.IsZero() { + return ErrInvalidArgument("Modified since cannot be empty.") + } + c.Set("If-Unmodified-Since", modTime.Format(http.TimeFormat)) + return nil +} + +// SetModified - set modified time since. +func (c GetConditions) SetModified(modTime time.Time) error { + if modTime.IsZero() { + return ErrInvalidArgument("Modified since cannot be empty.") + } + c.Set("If-Modified-Since", modTime.Format(http.TimeFormat)) + return nil +} + +// SetRange - set the start and end offset of the object to be read. +// See https://tools.ietf.org/html/rfc7233#section-3.1 for reference. +func (c GetConditions) SetRange(start, end int64) error { + if start < 0 || end > 0 && end < start { + return ErrInvalidArgument("Range start less than 0 or range end less than range start.") + } + if end == 0 { + // Read everything starting from offset 'start'. + c.Set("Range", fmt.Sprintf("bytes=%d-", start)) + } else { + // Read everything starting at 'start' till the 'end'. + c.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) + } + return nil +} diff --git a/api-get-object-file.go b/api-get-object-file.go index a38fc852a7..5a8da2c54c 100644 --- a/api-get-object-file.go +++ b/api-get-object-file.go @@ -78,8 +78,13 @@ func (c Client) FGetObject(bucketName, objectName, filePath string) error { return err } + // Initialize get object conditions and set the appropriate + // range offsets to read from. + conds := GetConditions{} + conds.SetRange(st.Size(), 0) + // Seek to current position for incoming reader. - objectReader, objectStat, err := c.getObject(bucketName, objectName, st.Size(), 0) + objectReader, objectStat, err := c.getObject(bucketName, objectName, conds) if err != nil { return err } diff --git a/api-get-object-partial.go b/api-get-object-partial.go new file mode 100644 index 0000000000..82d908d237 --- /dev/null +++ b/api-get-object-partial.go @@ -0,0 +1,17 @@ +package minio + +/* + * Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/api-get-object.go b/api-get-object.go index 8066f70f23..cc4f62d3f7 100644 --- a/api-get-object.go +++ b/api-get-object.go @@ -1,5 +1,5 @@ /* - * Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015, 2016 Minio, Inc. + * Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015, 2016, 2017 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,16 +97,19 @@ func (c Client) GetObject(bucketName, objectName string) (*Object, error) { if req.isFirstReq { // First request is a Read/ReadAt. if req.isReadOp { + conds := GetConditions{} // Differentiate between wanting the whole object and just a range. if req.isReadAt { // If this is a ReadAt request only get the specified range. // Range is set with respect to the offset and length of the buffer requested. // Do not set objectInfo from the first readAt request because it will not get // the whole object. - httpReader, _, err = c.getObject(bucketName, objectName, req.Offset, int64(len(req.Buffer))) + conds.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1) + httpReader, _, err = c.getObject(bucketName, objectName, conds) } else { + conds.SetRange(req.Offset, 0) // First request is a Read request. - httpReader, objectInfo, err = c.getObject(bucketName, objectName, req.Offset, 0) + httpReader, objectInfo, err = c.getObject(bucketName, objectName, conds) } if err != nil { resCh <- getResponse{ @@ -166,6 +169,7 @@ func (c Client) GetObject(bucketName, objectName string) (*Object, error) { // new ones when they haven't been already. // All readAt requests are new requests. if req.DidOffsetChange || !req.beenRead { + conds := GetConditions{} if httpReader != nil { // Close previously opened http reader. httpReader.Close() @@ -173,9 +177,11 @@ func (c Client) GetObject(bucketName, objectName string) (*Object, error) { // If this request is a readAt only get the specified range. if req.isReadAt { // Range is set with respect to the offset and length of the buffer requested. - httpReader, _, err = c.getObject(bucketName, objectName, req.Offset, int64(len(req.Buffer))) + conds.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1) + httpReader, _, err = c.getObject(bucketName, objectName, conds) } else { - httpReader, objectInfo, err = c.getObject(bucketName, objectName, req.Offset, 0) + conds.SetRange(req.Offset, 0) + httpReader, objectInfo, err = c.getObject(bucketName, objectName, conds) } if err != nil { resCh <- getResponse{ @@ -230,8 +236,8 @@ type getResponse struct { objectInfo ObjectInfo // Used for the first request. } -// Object represents an open object. It implements Read, ReadAt, -// Seeker, Close for a HTTP stream. +// Object represents an open object. It implements +// Reader, ReaderAt, Seeker, Closer for a HTTP stream. type Object struct { // Mutex. mutex *sync.Mutex @@ -594,7 +600,7 @@ func newObject(reqCh chan<- getRequest, resCh <-chan getResponse, doneCh chan<- // // For more information about the HTTP Range header. // go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35. -func (c Client) getObject(bucketName, objectName string, offset, length int64) (io.ReadCloser, ObjectInfo, error) { +func (c Client) getObject(bucketName, objectName string, conditions GetConditions) (io.ReadCloser, ObjectInfo, error) { // Validate input arguments. if err := isValidBucketName(bucketName); err != nil { return nil, ObjectInfo{}, err @@ -603,15 +609,10 @@ func (c Client) getObject(bucketName, objectName string, offset, length int64) ( return nil, ObjectInfo{}, err } + // Set all the necessary conditions. customHeader := make(http.Header) - // Set ranges if length and offset are valid. - // See https://tools.ietf.org/html/rfc7233#section-3.1 for reference. - if length > 0 && offset >= 0 { - customHeader.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) - } else if offset > 0 && length == 0 { - customHeader.Set("Range", fmt.Sprintf("bytes=%d-", offset)) - } else if length < 0 && offset == 0 { - customHeader.Set("Range", fmt.Sprintf("bytes=%d", length)) + for key, value := range conditions.Header { + customHeader[key] = value } // Execute GET on objectName. @@ -645,6 +646,7 @@ func (c Client) getObject(bucketName, objectName string, offset, length int64) ( Region: resp.Header.Get("x-amz-bucket-region"), } } + // Get content-type. contentType := strings.TrimSpace(resp.Header.Get("Content-Type")) if contentType == "" { @@ -656,7 +658,5 @@ func (c Client) getObject(bucketName, objectName string, offset, length int64) ( objectStat.Size = resp.ContentLength objectStat.LastModified = date objectStat.ContentType = contentType - - // do not close body here, caller will close return resp.Body, objectStat, nil } diff --git a/core.go b/core.go index 90154d945b..c824710505 100644 --- a/core.go +++ b/core.go @@ -98,3 +98,10 @@ func (c Core) GetBucketPolicy(bucket string) (policy.BucketAccessPolicy, error) func (c Core) PutBucketPolicy(bucket string, bucketPolicy policy.BucketAccessPolicy) error { return c.putBucketPolicy(bucket, bucketPolicy) } + +// GetObject is a lower level API implemented to support reading +// partial objects and also downloading objects with special conditions +// matching etag, modtime etc. +func (c Core) GetObject(bucketName, objectName string, conditions GetConditions) (io.ReadCloser, ObjectInfo, error) { + return c.getObject(bucketName, objectName, conditions) +}