/* * Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 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 ( "crypto/md5" "crypto/sha256" "encoding/hex" "fmt" "hash" "io" "io/ioutil" "os" "sort" ) // getUploadID - fetch upload id if already present for an object name // or initiate a new request to fetch a new upload id. func (c Client) getUploadID(bucketName, objectName, contentType string) (string, error) { // Input validation. if err := isValidBucketName(bucketName); err != nil { return "", err } if err := isValidObjectName(objectName); err != nil { return "", err } // Set content Type to default if empty string. if contentType == "" { contentType = "application/octet-stream" } // Find upload id for previous upload for an object. uploadID, err := c.findUploadID(bucketName, objectName) if err != nil { return "", err } if uploadID == "" { // Initiate multipart upload for an object. initMultipartUploadResult, err := c.initiateMultipartUpload(bucketName, objectName, contentType) if err != nil { return "", err } // Save the new upload id. uploadID = initMultipartUploadResult.UploadID } return uploadID, nil } // computeHash - Calculates MD5 and SHA256 for an input read Seeker. func (c Client) computeHash(reader io.ReadSeeker) (md5Sum, sha256Sum []byte, size int64, err error) { // MD5 and SHA256 hasher. var hashMD5, hashSHA256 hash.Hash // MD5 and SHA256 hasher. hashMD5 = md5.New() hashWriter := io.MultiWriter(hashMD5) if c.signature.isV4() { hashSHA256 = sha256.New() hashWriter = io.MultiWriter(hashMD5, hashSHA256) } size, err = io.Copy(hashWriter, reader) if err != nil { return nil, nil, 0, err } // Seek back reader to the beginning location. if _, err := reader.Seek(0, 0); err != nil { return nil, nil, 0, err } // Finalize md5shum and sha256 sum. md5Sum = hashMD5.Sum(nil) if c.signature.isV4() { sha256Sum = hashSHA256.Sum(nil) } return md5Sum, sha256Sum, size, nil } // FPutObject - Create an object in a bucket, with contents from file at filePath. func (c Client) FPutObject(bucketName, objectName, filePath, contentType string) (n int64, err error) { // Input validation. if err := isValidBucketName(bucketName); err != nil { return 0, err } if err := isValidObjectName(objectName); err != nil { return 0, err } // Open the referenced file. fileReader, err := os.Open(filePath) // If any error fail quickly here. if err != nil { return 0, err } defer fileReader.Close() // Save the file stat. fileStat, err := fileReader.Stat() if err != nil { return 0, err } // Save the file size. fileSize := fileStat.Size() // Check for largest object size allowed. if fileSize > int64(maxMultipartPutObjectSize) { return 0, ErrEntityTooLarge(fileSize, bucketName, objectName) } // NOTE: Google Cloud Storage multipart Put is not compatible with Amazon S3 APIs. // Current implementation will only upload a maximum of 5GiB to Google Cloud Storage servers. if isGoogleEndpoint(c.endpointURL) { if fileSize > int64(maxSinglePutObjectSize) { return 0, ErrorResponse{ Code: "NotImplemented", Message: fmt.Sprintf("Invalid Content-Length %d for file uploads to Google Cloud Storage.", fileSize), Key: objectName, BucketName: bucketName, } } // Do not compute MD5 for Google Cloud Storage. Uploads upto 5GiB in size. return c.putObjectNoChecksum(bucketName, objectName, fileReader, fileSize, contentType) } // NOTE: S3 doesn't allow anonymous multipart requests. if isAmazonEndpoint(c.endpointURL) && c.anonymous { if fileSize > int64(maxSinglePutObjectSize) { return 0, ErrorResponse{ Code: "NotImplemented", Message: fmt.Sprintf("For anonymous requests Content-Length cannot be %d.", fileSize), Key: objectName, BucketName: bucketName, } } // Do not compute MD5 for anonymous requests to Amazon S3. Uploads upto 5GiB in size. return c.putObjectNoChecksum(bucketName, objectName, fileReader, fileSize, contentType) } // Small object upload is initiated for uploads for input data size smaller than 5MiB. if fileSize < minimumPartSize { return c.putObjectSingle(bucketName, objectName, fileReader, fileSize, contentType) } // Upload all large objects as multipart. n, err = c.putObjectMultipartFromFile(bucketName, objectName, fileReader, fileSize, contentType) if err != nil { errResp := ToErrorResponse(err) // Verify if multipart functionality is not available, if not // fall back to single PutObject operation. if errResp.Code == "NotImplemented" { // If size of file is greater than '5GiB' fail. if fileSize > maxSinglePutObjectSize { return 0, ErrEntityTooLarge(fileSize, bucketName, objectName) } // Fall back to uploading as single PutObject operation. return c.putObjectSingle(bucketName, objectName, fileReader, fileSize, contentType) } return n, err } return n, nil } // putObjectMultipartFromFile - Creates object from contents of *os.File // // NOTE: This function is meant to be used for readers with local // file as in *os.File. This function resumes by skipping all the // necessary parts which were already uploaded by verifying them // against MD5SUM of each individual parts. This function also // effectively utilizes file system capabilities of reading from // specific sections and not having to create temporary files. func (c Client) putObjectMultipartFromFile(bucketName, objectName string, fileReader *os.File, fileSize int64, contentType string) (int64, error) { // Input validation. if err := isValidBucketName(bucketName); err != nil { return 0, err } if err := isValidObjectName(objectName); err != nil { return 0, err } // Get upload id for an object, initiates a new multipart request // if it cannot find any previously partially uploaded object. uploadID, err := c.getUploadID(bucketName, objectName, contentType) if err != nil { return 0, err } // Total data read and written to server. should be equal to 'size' at the end of the call. var totalUploadedSize int64 // Complete multipart upload. var completeMultipartUpload completeMultipartUpload // Fetch previously upload parts. partsInfo, err := c.listObjectParts(bucketName, objectName, uploadID) if err != nil { return 0, err } // Previous maximum part size var prevMaxPartSize int64 // Loop through all parts and fetch prevMaxPartSize. for _, partInfo := range partsInfo { // Choose the maximum part size. if partInfo.Size >= prevMaxPartSize { prevMaxPartSize = partInfo.Size } } // Calculate the optimal part size for a given file size. partSize := optimalPartSize(fileSize) // Use prevMaxPartSize if available. if prevMaxPartSize != 0 { partSize = prevMaxPartSize } // Part number always starts with '0'. partNumber := 0 // Upload each part until fileSize. for totalUploadedSize < fileSize { // Increment part number. partNumber++ // Get a section reader on a particular offset. sectionReader := io.NewSectionReader(fileReader, totalUploadedSize, partSize) // Calculates MD5 and SHA256 sum for a section reader. md5Sum, sha256Sum, size, err := c.computeHash(sectionReader) if err != nil { return 0, err } // Verify if part was not uploaded. if !isPartUploaded(objectPart{ ETag: hex.EncodeToString(md5Sum), PartNumber: partNumber, }, partsInfo) { // Proceed to upload the part. objPart, err := c.uploadPart(bucketName, objectName, uploadID, ioutil.NopCloser(sectionReader), partNumber, md5Sum, sha256Sum, size) if err != nil { return totalUploadedSize, err } // Save successfully uploaded part metadata. partsInfo[partNumber] = objPart } // Save successfully uploaded size. totalUploadedSize += size } // Verify if we uploaded all data. if totalUploadedSize != fileSize { return totalUploadedSize, ErrUnexpectedEOF(totalUploadedSize, fileSize, bucketName, objectName) } // Loop over uploaded parts to save them in a Parts array before completing the multipart request. for _, part := range partsInfo { var complPart completePart complPart.ETag = part.ETag complPart.PartNumber = part.PartNumber completeMultipartUpload.Parts = append(completeMultipartUpload.Parts, complPart) } // Verify if partNumber is different than total list of parts. if partNumber != len(completeMultipartUpload.Parts) { return totalUploadedSize, ErrInvalidParts(partNumber, len(completeMultipartUpload.Parts)) } // Sort all completed parts. sort.Sort(completedParts(completeMultipartUpload.Parts)) _, err = c.completeMultipartUpload(bucketName, objectName, uploadID, completeMultipartUpload) if err != nil { return totalUploadedSize, err } // Return final size. return totalUploadedSize, nil }