/* * Minio 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 donut import ( "encoding/base64" "encoding/hex" "encoding/json" "encoding/xml" "fmt" "io" "io/ioutil" "math/rand" "os" "path/filepath" "sort" "strconv" "strings" "time" "github.com/minio/minio/pkg/crypto/sha256" "github.com/minio/minio/pkg/crypto/sha512" "github.com/minio/minio/pkg/donut/disk" "github.com/minio/minio/pkg/probe" signv4 "github.com/minio/minio/pkg/signature" ) // config files used inside Donut const ( // bucket, object metadata bucketMetadataConfig = "bucketMetadata.json" objectMetadataConfig = "objectMetadata.json" // versions objectMetadataVersion = "1.0.0" bucketMetadataVersion = "1.0.0" ) /// v1 API functions // makeBucket - make a new bucket func (donut API) makeBucket(bucket string, acl BucketACL) *probe.Error { if bucket == "" || strings.TrimSpace(bucket) == "" { return probe.NewError(InvalidArgument{}) } return donut.makeDonutBucket(bucket, acl.String()) } // getBucketMetadata - get bucket metadata func (donut API) getBucketMetadata(bucketName string) (BucketMetadata, *probe.Error) { if err := donut.listDonutBuckets(); err != nil { return BucketMetadata{}, err.Trace() } if _, ok := donut.buckets[bucketName]; !ok { return BucketMetadata{}, probe.NewError(BucketNotFound{Bucket: bucketName}) } metadata, err := donut.getDonutBucketMetadata() if err != nil { return BucketMetadata{}, err.Trace() } return metadata.Buckets[bucketName], nil } // setBucketMetadata - set bucket metadata func (donut API) setBucketMetadata(bucketName string, bucketMetadata map[string]string) *probe.Error { if err := donut.listDonutBuckets(); err != nil { return err.Trace() } metadata, err := donut.getDonutBucketMetadata() if err != nil { return err.Trace() } oldBucketMetadata := metadata.Buckets[bucketName] acl, ok := bucketMetadata["acl"] if !ok { return probe.NewError(InvalidArgument{}) } oldBucketMetadata.ACL = BucketACL(acl) metadata.Buckets[bucketName] = oldBucketMetadata return donut.setDonutBucketMetadata(metadata) } // listBuckets - return list of buckets func (donut API) listBuckets() (map[string]BucketMetadata, *probe.Error) { if err := donut.listDonutBuckets(); err != nil { return nil, err.Trace() } metadata, err := donut.getDonutBucketMetadata() if err != nil { // intentionally left out the error when Donut is empty // but we need to revisit this area in future - since we need // to figure out between acceptable and unacceptable errors return make(map[string]BucketMetadata), nil } if metadata == nil { return make(map[string]BucketMetadata), nil } return metadata.Buckets, nil } // listObjects - return list of objects func (donut API) listObjects(bucket, prefix, marker, delimiter string, maxkeys int) (ListObjectsResults, *probe.Error) { if err := donut.listDonutBuckets(); err != nil { return ListObjectsResults{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return ListObjectsResults{}, probe.NewError(BucketNotFound{Bucket: bucket}) } listObjects, err := donut.buckets[bucket].ListObjects(prefix, marker, delimiter, maxkeys) if err != nil { return ListObjectsResults{}, err.Trace() } return listObjects, nil } // putObject - put object func (donut API) putObject(bucket, object, expectedMD5Sum string, reader io.Reader, size int64, metadata map[string]string, signature *signv4.Signature) (ObjectMetadata, *probe.Error) { if bucket == "" || strings.TrimSpace(bucket) == "" { return ObjectMetadata{}, probe.NewError(InvalidArgument{}) } if object == "" || strings.TrimSpace(object) == "" { return ObjectMetadata{}, probe.NewError(InvalidArgument{}) } if err := donut.listDonutBuckets(); err != nil { return ObjectMetadata{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return ObjectMetadata{}, probe.NewError(BucketNotFound{Bucket: bucket}) } bucketMeta, err := donut.getDonutBucketMetadata() if err != nil { return ObjectMetadata{}, err.Trace() } if _, ok := bucketMeta.Buckets[bucket].BucketObjects[object]; ok { return ObjectMetadata{}, probe.NewError(ObjectExists{Object: object}) } objMetadata, err := donut.buckets[bucket].WriteObject(object, reader, size, expectedMD5Sum, metadata, signature) if err != nil { return ObjectMetadata{}, err.Trace() } bucketMeta.Buckets[bucket].BucketObjects[object] = struct{}{} if err := donut.setDonutBucketMetadata(bucketMeta); err != nil { return ObjectMetadata{}, err.Trace() } return objMetadata, nil } // putObject - put object func (donut API) putObjectPart(bucket, object, expectedMD5Sum, uploadID string, partID int, reader io.Reader, size int64, metadata map[string]string, signature *signv4.Signature) (PartMetadata, *probe.Error) { if bucket == "" || strings.TrimSpace(bucket) == "" { return PartMetadata{}, probe.NewError(InvalidArgument{}) } if object == "" || strings.TrimSpace(object) == "" { return PartMetadata{}, probe.NewError(InvalidArgument{}) } if err := donut.listDonutBuckets(); err != nil { return PartMetadata{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return PartMetadata{}, probe.NewError(BucketNotFound{Bucket: bucket}) } bucketMeta, err := donut.getDonutBucketMetadata() if err != nil { return PartMetadata{}, err.Trace() } if _, ok := bucketMeta.Buckets[bucket].Multiparts[object]; !ok { return PartMetadata{}, probe.NewError(InvalidUploadID{UploadID: uploadID}) } if _, ok := bucketMeta.Buckets[bucket].BucketObjects[object]; ok { return PartMetadata{}, probe.NewError(ObjectExists{Object: object}) } objectPart := object + "/" + "multipart" + "/" + strconv.Itoa(partID) objmetadata, err := donut.buckets[bucket].WriteObject(objectPart, reader, size, expectedMD5Sum, metadata, signature) if err != nil { return PartMetadata{}, err.Trace() } partMetadata := PartMetadata{ PartNumber: partID, LastModified: objmetadata.Created, ETag: objmetadata.MD5Sum, Size: objmetadata.Size, } multipartSession := bucketMeta.Buckets[bucket].Multiparts[object] multipartSession.Parts[strconv.Itoa(partID)] = partMetadata bucketMeta.Buckets[bucket].Multiparts[object] = multipartSession if err := donut.setDonutBucketMetadata(bucketMeta); err != nil { return PartMetadata{}, err.Trace() } return partMetadata, nil } // getObject - get object func (donut API) getObject(bucket, object string) (reader io.ReadCloser, size int64, err *probe.Error) { if bucket == "" || strings.TrimSpace(bucket) == "" { return nil, 0, probe.NewError(InvalidArgument{}) } if object == "" || strings.TrimSpace(object) == "" { return nil, 0, probe.NewError(InvalidArgument{}) } if err := donut.listDonutBuckets(); err != nil { return nil, 0, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return nil, 0, probe.NewError(BucketNotFound{Bucket: bucket}) } return donut.buckets[bucket].ReadObject(object) } // getObjectMetadata - get object metadata func (donut API) getObjectMetadata(bucket, object string) (ObjectMetadata, *probe.Error) { if err := donut.listDonutBuckets(); err != nil { return ObjectMetadata{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return ObjectMetadata{}, probe.NewError(BucketNotFound{Bucket: bucket}) } bucketMeta, err := donut.getDonutBucketMetadata() if err != nil { return ObjectMetadata{}, err.Trace() } if _, ok := bucketMeta.Buckets[bucket].BucketObjects[object]; !ok { return ObjectMetadata{}, probe.NewError(ObjectNotFound{Object: object}) } objectMetadata, err := donut.buckets[bucket].GetObjectMetadata(object) if err != nil { return ObjectMetadata{}, err.Trace() } return objectMetadata, nil } // newMultipartUpload - new multipart upload request func (donut API) newMultipartUpload(bucket, object, contentType string) (string, *probe.Error) { if err := donut.listDonutBuckets(); err != nil { return "", err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return "", probe.NewError(BucketNotFound{Bucket: bucket}) } allbuckets, err := donut.getDonutBucketMetadata() if err != nil { return "", err.Trace() } bucketMetadata := allbuckets.Buckets[bucket] multiparts := make(map[string]MultiPartSession) if len(bucketMetadata.Multiparts) > 0 { multiparts = bucketMetadata.Multiparts } id := []byte(strconv.Itoa(rand.Int()) + bucket + object + time.Now().String()) uploadIDSum := sha512.Sum512(id) uploadID := base64.URLEncoding.EncodeToString(uploadIDSum[:])[:47] multipartSession := MultiPartSession{ UploadID: uploadID, Initiated: time.Now().UTC(), Parts: make(map[string]PartMetadata), TotalParts: 0, } multiparts[object] = multipartSession bucketMetadata.Multiparts = multiparts allbuckets.Buckets[bucket] = bucketMetadata if err := donut.setDonutBucketMetadata(allbuckets); err != nil { return "", err.Trace() } return uploadID, nil } // listObjectParts list all object parts func (donut API) listObjectParts(bucket, object string, resources ObjectResourcesMetadata) (ObjectResourcesMetadata, *probe.Error) { if bucket == "" || strings.TrimSpace(bucket) == "" { return ObjectResourcesMetadata{}, probe.NewError(InvalidArgument{}) } if object == "" || strings.TrimSpace(object) == "" { return ObjectResourcesMetadata{}, probe.NewError(InvalidArgument{}) } if err := donut.listDonutBuckets(); err != nil { return ObjectResourcesMetadata{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return ObjectResourcesMetadata{}, probe.NewError(BucketNotFound{Bucket: bucket}) } allBuckets, err := donut.getDonutBucketMetadata() if err != nil { return ObjectResourcesMetadata{}, err.Trace() } bucketMetadata := allBuckets.Buckets[bucket] if _, ok := bucketMetadata.Multiparts[object]; !ok { return ObjectResourcesMetadata{}, probe.NewError(InvalidUploadID{UploadID: resources.UploadID}) } if bucketMetadata.Multiparts[object].UploadID != resources.UploadID { return ObjectResourcesMetadata{}, probe.NewError(InvalidUploadID{UploadID: resources.UploadID}) } objectResourcesMetadata := resources objectResourcesMetadata.Bucket = bucket objectResourcesMetadata.Key = object var parts []*PartMetadata var startPartNumber int switch { case objectResourcesMetadata.PartNumberMarker == 0: startPartNumber = 1 default: startPartNumber = objectResourcesMetadata.PartNumberMarker } for i := startPartNumber; i <= bucketMetadata.Multiparts[object].TotalParts; i++ { if len(parts) > objectResourcesMetadata.MaxParts { sort.Sort(partNumber(parts)) objectResourcesMetadata.IsTruncated = true objectResourcesMetadata.Part = parts objectResourcesMetadata.NextPartNumberMarker = i return objectResourcesMetadata, nil } part, ok := bucketMetadata.Multiparts[object].Parts[strconv.Itoa(i)] if !ok { return ObjectResourcesMetadata{}, probe.NewError(InvalidPart{}) } parts = append(parts, &part) } sort.Sort(partNumber(parts)) objectResourcesMetadata.Part = parts return objectResourcesMetadata, nil } // completeMultipartUpload complete an incomplete multipart upload func (donut API) completeMultipartUpload(bucket, object, uploadID string, data io.Reader, signature *signv4.Signature) (ObjectMetadata, *probe.Error) { if bucket == "" || strings.TrimSpace(bucket) == "" { return ObjectMetadata{}, probe.NewError(InvalidArgument{}) } if object == "" || strings.TrimSpace(object) == "" { return ObjectMetadata{}, probe.NewError(InvalidArgument{}) } if err := donut.listDonutBuckets(); err != nil { return ObjectMetadata{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return ObjectMetadata{}, probe.NewError(BucketNotFound{Bucket: bucket}) } allBuckets, err := donut.getDonutBucketMetadata() if err != nil { return ObjectMetadata{}, err.Trace() } bucketMetadata := allBuckets.Buckets[bucket] if _, ok := bucketMetadata.Multiparts[object]; !ok { return ObjectMetadata{}, probe.NewError(InvalidUploadID{UploadID: uploadID}) } if bucketMetadata.Multiparts[object].UploadID != uploadID { return ObjectMetadata{}, probe.NewError(InvalidUploadID{UploadID: uploadID}) } var partBytes []byte { var err error partBytes, err = ioutil.ReadAll(data) if err != nil { return ObjectMetadata{}, probe.NewError(err) } } if signature != nil { ok, err := signature.DoesSignatureMatch(hex.EncodeToString(sha256.Sum256(partBytes)[:])) if err != nil { return ObjectMetadata{}, err.Trace() } if !ok { return ObjectMetadata{}, probe.NewError(signv4.DoesNotMatch{}) } } parts := &CompleteMultipartUpload{} if err := xml.Unmarshal(partBytes, parts); err != nil { return ObjectMetadata{}, probe.NewError(MalformedXML{}) } if !sort.IsSorted(completedParts(parts.Part)) { return ObjectMetadata{}, probe.NewError(InvalidPartOrder{}) } for _, part := range parts.Part { if strings.Trim(part.ETag, "\"") != bucketMetadata.Multiparts[object].Parts[strconv.Itoa(part.PartNumber)].ETag { return ObjectMetadata{}, probe.NewError(InvalidPart{}) } } var finalETagBytes []byte var finalSize int64 totalParts := strconv.Itoa(bucketMetadata.Multiparts[object].TotalParts) for _, part := range bucketMetadata.Multiparts[object].Parts { partETagBytes, err := hex.DecodeString(part.ETag) if err != nil { return ObjectMetadata{}, probe.NewError(err) } finalETagBytes = append(finalETagBytes, partETagBytes...) finalSize += part.Size } finalETag := hex.EncodeToString(finalETagBytes) objMetadata := ObjectMetadata{} objMetadata.MD5Sum = finalETag + "-" + totalParts objMetadata.Object = object objMetadata.Bucket = bucket objMetadata.Size = finalSize objMetadata.Created = bucketMetadata.Multiparts[object].Parts[totalParts].LastModified return objMetadata, nil } // listMultipartUploads list all multipart uploads func (donut API) listMultipartUploads(bucket string, resources BucketMultipartResourcesMetadata) (BucketMultipartResourcesMetadata, *probe.Error) { if err := donut.listDonutBuckets(); err != nil { return BucketMultipartResourcesMetadata{}, err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return BucketMultipartResourcesMetadata{}, probe.NewError(BucketNotFound{Bucket: bucket}) } allbuckets, err := donut.getDonutBucketMetadata() if err != nil { return BucketMultipartResourcesMetadata{}, err.Trace() } bucketMetadata := allbuckets.Buckets[bucket] var uploads []*UploadMetadata for key, session := range bucketMetadata.Multiparts { if strings.HasPrefix(key, resources.Prefix) { if len(uploads) > resources.MaxUploads { sort.Sort(byKey(uploads)) resources.Upload = uploads resources.NextKeyMarker = key resources.NextUploadIDMarker = session.UploadID resources.IsTruncated = true return resources, nil } // uploadIDMarker is ignored if KeyMarker is empty switch { case resources.KeyMarker != "" && resources.UploadIDMarker == "": if key > resources.KeyMarker { upload := new(UploadMetadata) upload.Key = key upload.UploadID = session.UploadID upload.Initiated = session.Initiated uploads = append(uploads, upload) } case resources.KeyMarker != "" && resources.UploadIDMarker != "": if session.UploadID > resources.UploadIDMarker { if key >= resources.KeyMarker { upload := new(UploadMetadata) upload.Key = key upload.UploadID = session.UploadID upload.Initiated = session.Initiated uploads = append(uploads, upload) } } default: upload := new(UploadMetadata) upload.Key = key upload.UploadID = session.UploadID upload.Initiated = session.Initiated uploads = append(uploads, upload) } } } sort.Sort(byKey(uploads)) resources.Upload = uploads return resources, nil } // abortMultipartUpload - abort a incomplete multipart upload func (donut API) abortMultipartUpload(bucket, object, uploadID string) *probe.Error { if err := donut.listDonutBuckets(); err != nil { return err.Trace() } if _, ok := donut.buckets[bucket]; !ok { return probe.NewError(BucketNotFound{Bucket: bucket}) } allbuckets, err := donut.getDonutBucketMetadata() if err != nil { return err.Trace() } bucketMetadata := allbuckets.Buckets[bucket] if _, ok := bucketMetadata.Multiparts[object]; !ok { return probe.NewError(InvalidUploadID{UploadID: uploadID}) } if bucketMetadata.Multiparts[object].UploadID != uploadID { return probe.NewError(InvalidUploadID{UploadID: uploadID}) } delete(bucketMetadata.Multiparts, object) allbuckets.Buckets[bucket] = bucketMetadata if err := donut.setDonutBucketMetadata(allbuckets); err != nil { return err.Trace() } return nil } //// internal functions // getBucketMetadataWriters - func (donut API) getBucketMetadataWriters() ([]io.WriteCloser, *probe.Error) { var writers []io.WriteCloser for _, node := range donut.nodes { disks, err := node.ListDisks() if err != nil { return nil, err.Trace() } writers = make([]io.WriteCloser, len(disks)) for order, disk := range disks { bucketMetaDataWriter, err := disk.CreateFile(filepath.Join(donut.config.DonutName, bucketMetadataConfig)) if err != nil { return nil, err.Trace() } writers[order] = bucketMetaDataWriter } } return writers, nil } // getBucketMetadataReaders - readers are returned in map rather than slice func (donut API) getBucketMetadataReaders() (map[int]io.ReadCloser, *probe.Error) { readers := make(map[int]io.ReadCloser) disks := make(map[int]disk.Disk) var err *probe.Error for _, node := range donut.nodes { nDisks := make(map[int]disk.Disk) nDisks, err = node.ListDisks() if err != nil { return nil, err.Trace() } for k, v := range nDisks { disks[k] = v } } var bucketMetaDataReader io.ReadCloser for order, disk := range disks { bucketMetaDataReader, err = disk.Open(filepath.Join(donut.config.DonutName, bucketMetadataConfig)) if err != nil { continue } readers[order] = bucketMetaDataReader } if err != nil { return nil, err.Trace() } return readers, nil } // setDonutBucketMetadata - func (donut API) setDonutBucketMetadata(metadata *AllBuckets) *probe.Error { writers, err := donut.getBucketMetadataWriters() if err != nil { return err.Trace() } for _, writer := range writers { jenc := json.NewEncoder(writer) if err := jenc.Encode(metadata); err != nil { CleanupWritersOnError(writers) return probe.NewError(err) } } for _, writer := range writers { writer.Close() } return nil } // getDonutBucketMetadata - func (donut API) getDonutBucketMetadata() (*AllBuckets, *probe.Error) { metadata := &AllBuckets{} readers, err := donut.getBucketMetadataReaders() if err != nil { return nil, err.Trace() } for _, reader := range readers { defer reader.Close() } { var err error for _, reader := range readers { jenc := json.NewDecoder(reader) if err = jenc.Decode(metadata); err == nil { return metadata, nil } } return nil, probe.NewError(err) } } // makeDonutBucket - func (donut API) makeDonutBucket(bucketName, acl string) *probe.Error { if err := donut.listDonutBuckets(); err != nil { return err.Trace() } if _, ok := donut.buckets[bucketName]; ok { return probe.NewError(BucketExists{Bucket: bucketName}) } bkt, bucketMetadata, err := newBucket(bucketName, acl, donut.config.DonutName, donut.nodes) if err != nil { return err.Trace() } nodeNumber := 0 donut.buckets[bucketName] = bkt for _, node := range donut.nodes { disks := make(map[int]disk.Disk) disks, err = node.ListDisks() if err != nil { return err.Trace() } for order, disk := range disks { bucketSlice := fmt.Sprintf("%s$%d$%d", bucketName, nodeNumber, order) err := disk.MakeDir(filepath.Join(donut.config.DonutName, bucketSlice)) if err != nil { return err.Trace() } } nodeNumber = nodeNumber + 1 } var metadata *AllBuckets metadata, err = donut.getDonutBucketMetadata() if err != nil { if os.IsNotExist(err.ToGoError()) { metadata = new(AllBuckets) metadata.Buckets = make(map[string]BucketMetadata) metadata.Buckets[bucketName] = bucketMetadata err = donut.setDonutBucketMetadata(metadata) if err != nil { return err.Trace() } return nil } return err.Trace() } metadata.Buckets[bucketName] = bucketMetadata err = donut.setDonutBucketMetadata(metadata) if err != nil { return err.Trace() } return nil } // listDonutBuckets - func (donut API) listDonutBuckets() *probe.Error { var disks map[int]disk.Disk var err *probe.Error for _, node := range donut.nodes { disks, err = node.ListDisks() if err != nil { return err.Trace() } } var dirs []os.FileInfo for _, disk := range disks { dirs, err = disk.ListDir(donut.config.DonutName) if err == nil { break } } // if all disks are missing then return error if err != nil { return err.Trace() } for _, dir := range dirs { splitDir := strings.Split(dir.Name(), "$") if len(splitDir) < 3 { return probe.NewError(CorruptedBackend{Backend: dir.Name()}) } bucketName := splitDir[0] // we dont need this once we cache from makeDonutBucket() bkt, _, err := newBucket(bucketName, "private", donut.config.DonutName, donut.nodes) if err != nil { return err.Trace() } donut.buckets[bucketName] = bkt } return nil }