/*
 * Minio Client (C) 2015, 2016, 2017, 2018 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 cmd

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"hash/fnv"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/minio/mc/pkg/httptracer"
	"github.com/minio/mc/pkg/probe"
	"github.com/minio/minio-go"
	"github.com/minio/minio-go/pkg/credentials"
	"github.com/minio/minio-go/pkg/encrypt"
	"github.com/minio/minio-go/pkg/policy"
	"github.com/minio/minio-go/pkg/s3utils"
	"github.com/minio/minio/pkg/mimedb"
)

// S3 client
type s3Client struct {
	mutex        *sync.Mutex
	targetURL    *clientURL
	api          *minio.Client
	virtualStyle bool
}

const (
	amazonHostName            = "s3.amazonaws.com"
	amazonHostNameAccelerated = "s3-accelerate.amazonaws.com"

	googleHostName            = "storage.googleapis.com"
	serverEncryptionKeyPrefix = "x-amz-server-side-encryption"
)

// cseHeaders is list of client side encryption headers
var cseHeaders = []string{
	"X-Amz-Meta-X-Amz-Iv",
	"X-Amz-Meta-X-Amz-Key",
	"X-Amz-Meta-X-Amz-Matdesc",
}

// newFactory encloses New function with client cache.
func newFactory() func(config *Config) (Client, *probe.Error) {
	clientCache := make(map[uint32]*minio.Client)
	mutex := &sync.Mutex{}

	// Return New function.
	return func(config *Config) (Client, *probe.Error) {
		// Creates a parsed URL.
		targetURL := newClientURL(config.HostURL)
		// By default enable HTTPs.
		useTLS := true
		if targetURL.Scheme == "http" {
			useTLS = false
		}

		// Instantiate s3
		s3Clnt := &s3Client{}
		// Allocate a new mutex.
		s3Clnt.mutex = new(sync.Mutex)
		// Save the target URL.
		s3Clnt.targetURL = targetURL

		// Save if target supports virtual host style.
		hostName := targetURL.Host
		s3Clnt.virtualStyle = isVirtualHostStyle(hostName, config.Lookup)
		isS3AcceleratedEndpoint := isAmazonAccelerated(hostName)

		if s3Clnt.virtualStyle {
			// If Amazon URL replace it with 's3.amazonaws.com'
			if isAmazon(hostName) || isAmazonAccelerated(hostName) {
				hostName = amazonHostName
			}

			// If Google URL replace it with 'storage.googleapis.com'
			if isGoogle(hostName) {
				hostName = googleHostName
			}
		}
		// Generate a hash out of s3Conf.
		confHash := fnv.New32a()
		confHash.Write([]byte(hostName + config.AccessKey + config.SecretKey))
		confSum := confHash.Sum32()

		// Lookup previous cache by hash.
		mutex.Lock()
		defer mutex.Unlock()
		var api *minio.Client
		var found bool
		if api, found = clientCache[confSum]; !found {
			// if Signature version '4' use NewV4 directly.
			creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "")
			// if Signature version '2' use NewV2 directly.
			if strings.ToUpper(config.Signature) == "S3V2" {
				creds = credentials.NewStaticV2(config.AccessKey, config.SecretKey, "")
			}
			// Not found. Instantiate a new minio
			var e error

			options := minio.Options{
				Creds:        creds,
				Secure:       useTLS,
				Region:       "",
				BucketLookup: config.Lookup,
			}
			api, e = minio.NewWithOptions(hostName, &options)
			if e != nil {
				return nil, probe.NewError(e)
			}

			// Keep TLS config.
			tlsConfig := &tls.Config{RootCAs: globalRootCAs}
			if config.Insecure {
				tlsConfig.InsecureSkipVerify = true
			}

			var transport http.RoundTripper = &http.Transport{
				Proxy: http.ProxyFromEnvironment,
				DialContext: (&net.Dialer{
					Timeout:   30 * time.Second,
					KeepAlive: 30 * time.Second,
				}).DialContext,
				MaxIdleConns:          1024,
				MaxIdleConnsPerHost:   1024,
				IdleConnTimeout:       90 * time.Second,
				TLSHandshakeTimeout:   10 * time.Second,
				ExpectContinueTimeout: 1 * time.Second,
				TLSClientConfig:       tlsConfig,
				// Set this value so that the underlying transport round-tripper
				// doesn't try to auto decode the body of objects with
				// content-encoding set to `gzip`.
				//
				// Refer:
				//    https://golang.org/src/net/http/transport.go?h=roundTrip#L1843
				DisableCompression: true,
			}

			if config.Debug {
				if strings.EqualFold(config.Signature, "S3v4") {
					transport = httptracer.GetNewTraceTransport(newTraceV4(), transport)
				} else if strings.EqualFold(config.Signature, "S3v2") {
					transport = httptracer.GetNewTraceTransport(newTraceV2(), transport)
				}
				// Set custom transport.
			}

			// Set custom transport.
			api.SetCustomTransport(transport)

			// If Amazon Accelerated URL is requested enable it.
			if isS3AcceleratedEndpoint {
				api.SetS3TransferAccelerate(amazonHostNameAccelerated)
			}

			// Set app info.
			api.SetAppInfo(config.AppName, config.AppVersion)

			// Cache the new minio client with hash of config as key.
			clientCache[confSum] = api
		}

		// Store the new api object.
		s3Clnt.api = api

		return s3Clnt, nil
	}
}

// s3New returns an initialized s3Client structure. If debug is enabled,
// it also enables an internal trace transport.
var s3New = newFactory()

// GetURL get url.
func (c *s3Client) GetURL() clientURL {
	return *c.targetURL
}

// Add bucket notification
func (c *s3Client) AddNotificationConfig(arn string, events []string, prefix, suffix string) *probe.Error {
	bucket, _ := c.url2BucketAndObject()
	// Validate total fields in ARN.
	fields := strings.Split(arn, ":")
	if len(fields) != 6 {
		return errInvalidArgument()
	}

	// Get any enabled notification.
	mb, e := c.api.GetBucketNotification(bucket)
	if e != nil {
		return probe.NewError(e)
	}

	accountArn := minio.NewArn(fields[1], fields[2], fields[3], fields[4], fields[5])
	nc := minio.NewNotificationConfig(accountArn)

	// Configure events
	for _, event := range events {
		switch event {
		case "put":
			nc.AddEvents(minio.ObjectCreatedAll)
		case "delete":
			nc.AddEvents(minio.ObjectRemovedAll)
		case "get":
			nc.AddEvents(minio.ObjectAccessedAll)
		default:
			return errInvalidArgument().Trace(events...)
		}
	}
	if prefix != "" {
		nc.AddFilterPrefix(prefix)
	}
	if suffix != "" {
		nc.AddFilterSuffix(suffix)
	}

	switch fields[2] {
	case "sns":
		if !mb.AddTopic(nc) {
			return errInvalidArgument().Trace("Overlapping Topic configs")
		}
	case "sqs":
		if !mb.AddQueue(nc) {
			return errInvalidArgument().Trace("Overlapping Queue configs")
		}
	case "lambda":
		if !mb.AddLambda(nc) {
			return errInvalidArgument().Trace("Overlapping lambda configs")
		}
	default:
		return errInvalidArgument().Trace(fields[2])
	}

	// Set the new bucket configuration
	if err := c.api.SetBucketNotification(bucket, mb); err != nil {
		return probe.NewError(err)
	}
	return nil
}

// Remove bucket notification
func (c *s3Client) RemoveNotificationConfig(arn string) *probe.Error {
	bucket, _ := c.url2BucketAndObject()
	// Remove all notification configs if arn is empty
	if arn == "" {
		if err := c.api.RemoveAllBucketNotification(bucket); err != nil {
			return probe.NewError(err)
		}
		return nil
	}

	mb, e := c.api.GetBucketNotification(bucket)
	if e != nil {
		return probe.NewError(e)
	}

	fields := strings.Split(arn, ":")
	if len(fields) != 6 {
		return errInvalidArgument().Trace(fields...)
	}
	accountArn := minio.NewArn(fields[1], fields[2], fields[3], fields[4], fields[5])

	switch fields[2] {
	case "sns":
		mb.RemoveTopicByArn(accountArn)
	case "sqs":
		mb.RemoveQueueByArn(accountArn)
	case "lambda":
		mb.RemoveLambdaByArn(accountArn)
	default:
		return errInvalidArgument().Trace(fields[2])
	}

	// Set the new bucket configuration
	if e := c.api.SetBucketNotification(bucket, mb); e != nil {
		return probe.NewError(e)
	}
	return nil
}

type notificationConfig struct {
	ID     string   `json:"id"`
	Arn    string   `json:"arn"`
	Events []string `json:"events"`
	Prefix string   `json:"prefix"`
	Suffix string   `json:"suffix"`
}

// List notification configs
func (c *s3Client) ListNotificationConfigs(arn string) ([]notificationConfig, *probe.Error) {
	var configs []notificationConfig
	bucket, _ := c.url2BucketAndObject()
	mb, e := c.api.GetBucketNotification(bucket)
	if e != nil {
		return nil, probe.NewError(e)
	}

	// Generate pretty event names from event types
	prettyEventNames := func(eventsTypes []minio.NotificationEventType) []string {
		var result []string
		for _, eventType := range eventsTypes {
			result = append(result, string(eventType))
		}
		return result
	}

	getFilters := func(config minio.NotificationConfig) (prefix, suffix string) {
		if config.Filter == nil {
			return
		}
		for _, filter := range config.Filter.S3Key.FilterRules {
			if strings.ToLower(filter.Name) == "prefix" {
				prefix = filter.Value
			}
			if strings.ToLower(filter.Name) == "suffix" {
				suffix = filter.Value
			}

		}
		return prefix, suffix
	}

	for _, config := range mb.TopicConfigs {
		if arn != "" && config.Topic != arn {
			continue
		}
		prefix, suffix := getFilters(config.NotificationConfig)
		configs = append(configs, notificationConfig{ID: config.ID,
			Arn:    config.Topic,
			Events: prettyEventNames(config.Events),
			Prefix: prefix,
			Suffix: suffix})
	}

	for _, config := range mb.QueueConfigs {
		if arn != "" && config.Queue != arn {
			continue
		}
		prefix, suffix := getFilters(config.NotificationConfig)
		configs = append(configs, notificationConfig{ID: config.ID,
			Arn:    config.Queue,
			Events: prettyEventNames(config.Events),
			Prefix: prefix,
			Suffix: suffix})
	}

	for _, config := range mb.LambdaConfigs {
		if arn != "" && config.Lambda != arn {
			continue
		}
		prefix, suffix := getFilters(config.NotificationConfig)
		configs = append(configs, notificationConfig{ID: config.ID,
			Arn:    config.Lambda,
			Events: prettyEventNames(config.Events),
			Prefix: prefix,
			Suffix: suffix})
	}

	return configs, nil
}

// Supported content types
var supportedContentTypes = []string{
	"csv",
	"json",
	"gzip",
	"bzip2",
}

func (c *s3Client) Select(expression string, sse encrypt.ServerSide) (io.ReadCloser, *probe.Error) {
	opts := minio.SelectObjectOptions{
		Expression:     expression,
		ExpressionType: minio.QueryExpressionTypeSQL,
		// Set any encryption headers
		ServerSideEncryption: sse,
	}

	bucket, object := c.url2BucketAndObject()
	ext := filepath.Ext(object)
	if strings.Contains(ext, "parquet") {
		opts.InputSerialization = minio.SelectObjectInputSerialization{
			CompressionType: minio.SelectCompressionNONE,
			Parquet:         &minio.ParquetInputOptions{},
		}
		opts.OutputSerialization = minio.SelectObjectOutputSerialization{
			JSON: &minio.JSONOutputOptions{
				RecordDelimiter: "\n",
			},
		}
	} else {
		origContentType := mimedb.TypeByExtension(filepath.Ext(strings.TrimSuffix(strings.TrimSuffix(object, ".gz"), ".bz2")))
		contentType := mimedb.TypeByExtension(ext)
		if strings.Contains(origContentType, "csv") {
			opts.InputSerialization = minio.SelectObjectInputSerialization{
				CompressionType: minio.SelectCompressionNONE,
				CSV: &minio.CSVInputOptions{
					FileHeaderInfo:  minio.CSVFileHeaderInfoUse,
					RecordDelimiter: "\n",
					FieldDelimiter:  ",",
				},
			}
			opts.OutputSerialization = minio.SelectObjectOutputSerialization{
				CSV: &minio.CSVOutputOptions{
					RecordDelimiter: "\n",
					FieldDelimiter:  ",",
				},
			}
		} else if strings.Contains(origContentType, "json") {
			opts.InputSerialization = minio.SelectObjectInputSerialization{
				CompressionType: minio.SelectCompressionNONE,
				JSON: &minio.JSONInputOptions{
					Type: minio.JSONLinesType,
				},
			}
			opts.OutputSerialization = minio.SelectObjectOutputSerialization{
				JSON: &minio.JSONOutputOptions{
					RecordDelimiter: "\n",
				},
			}
		}
		if strings.Contains(contentType, "gzip") {
			opts.InputSerialization.CompressionType = minio.SelectCompressionGZIP
		} else if strings.Contains(contentType, "bzip") {
			opts.InputSerialization.CompressionType = minio.SelectCompressionBZIP
		}
	}
	reader, e := c.api.SelectObjectContent(context.Background(), bucket, object, opts)
	if e != nil {
		return nil, probe.NewError(e)
	}
	return reader, nil
}

// Start watching on all bucket events for a given account ID.
func (c *s3Client) Watch(params watchParams) (*watchObject, *probe.Error) {
	eventChan := make(chan EventInfo)
	errorChan := make(chan *probe.Error)
	doneChan := make(chan bool)

	// Extract bucket and object.
	bucket, object := c.url2BucketAndObject()

	// Flag set to set the notification.
	var events []string
	for _, event := range params.events {
		switch event {
		case "put":
			events = append(events, string(minio.ObjectCreatedAll))
		case "delete":
			events = append(events, string(minio.ObjectRemovedAll))
		case "get":
			events = append(events, string(minio.ObjectAccessedAll))
		default:
			return nil, errInvalidArgument().Trace(event)
		}
	}
	if object != "" && params.prefix != "" {
		return nil, errInvalidArgument().Trace(params.prefix, object)
	}
	if object != "" && params.prefix == "" {
		params.prefix = object
	}

	doneCh := make(chan struct{})

	// wait for doneChan to close the other channels
	go func() {
		<-doneChan

		close(doneCh)
		close(eventChan)
		close(errorChan)
	}()

	// Start listening on all bucket events.
	eventsCh := c.api.ListenBucketNotification(bucket, params.prefix, params.suffix, events, doneCh)

	wo := &watchObject{
		eventInfoChan: eventChan,
		errorChan:     errorChan,
		doneChan:      doneChan,
	}

	// wait for events to occur and sent them through the eventChan and errorChan
	go func() {
		defer wo.Close()
		for notificationInfo := range eventsCh {
			if notificationInfo.Err != nil {
				if nErr, ok := notificationInfo.Err.(minio.ErrorResponse); ok && nErr.Code == "APINotSupported" {
					errorChan <- probe.NewError(APINotImplemented{
						API:     "Watch",
						APIType: c.targetURL.Scheme + "://" + c.targetURL.Host,
					})
					return
				}
				errorChan <- probe.NewError(notificationInfo.Err)
			}

			for _, record := range notificationInfo.Records {
				bucketName := record.S3.Bucket.Name
				key, e := url.QueryUnescape(record.S3.Object.Key)
				if e != nil {
					errorChan <- probe.NewError(e)
					continue
				}

				u := *c.targetURL
				u.Path = path.Join(string(u.Separator), bucketName, key)
				if strings.HasPrefix(record.EventName, "s3:ObjectCreated:") {
					eventChan <- EventInfo{
						Time:      record.EventTime,
						Size:      record.S3.Object.Size,
						Path:      u.String(),
						Type:      EventCreate,
						Host:      record.Source.Host,
						Port:      record.Source.Port,
						UserAgent: record.Source.UserAgent,
					}

				} else if strings.HasPrefix(record.EventName, "s3:ObjectRemoved:") {
					eventChan <- EventInfo{
						Time:      record.EventTime,
						Path:      u.String(),
						Type:      EventRemove,
						Host:      record.Source.Host,
						Port:      record.Source.Port,
						UserAgent: record.Source.UserAgent,
					}
				} else if record.EventName == minio.ObjectAccessedGet {
					eventChan <- EventInfo{
						Time:      record.EventTime,
						Size:      record.S3.Object.Size,
						Path:      u.String(),
						Type:      EventAccessedRead,
						Host:      record.Source.Host,
						Port:      record.Source.Port,
						UserAgent: record.Source.UserAgent,
					}
				} else if record.EventName == minio.ObjectAccessedHead {
					eventChan <- EventInfo{
						Time:      record.EventTime,
						Size:      record.S3.Object.Size,
						Path:      u.String(),
						Type:      EventAccessedStat,
						Host:      record.Source.Host,
						Port:      record.Source.Port,
						UserAgent: record.Source.UserAgent,
					}
				}
			}
		}
	}()

	return wo, nil
}

// Get - get object with metadata.
func (c *s3Client) Get(sse encrypt.ServerSide) (io.ReadCloser, *probe.Error) {
	bucket, object := c.url2BucketAndObject()
	opts := minio.GetObjectOptions{}
	opts.ServerSideEncryption = sse
	reader, e := c.api.GetObject(bucket, object, opts)
	if e != nil {
		errResponse := minio.ToErrorResponse(e)
		if errResponse.Code == "NoSuchBucket" {
			return nil, probe.NewError(BucketDoesNotExist{
				Bucket: bucket,
			})
		}
		if errResponse.Code == "InvalidBucketName" {
			return nil, probe.NewError(BucketInvalid{
				Bucket: bucket,
			})
		}
		if errResponse.Code == "NoSuchKey" {
			return nil, probe.NewError(ObjectMissing{})
		}
		return nil, probe.NewError(e)
	}
	return reader, nil
}

// Copy - copy object, uses server side copy API. Also uses an abstracted API
// such that large file sizes will be copied in multipart manner on server
// side.
func (c *s3Client) Copy(source string, size int64, progress io.Reader, srcSSE, tgtSSE encrypt.ServerSide) *probe.Error {
	dstBucket, dstObject := c.url2BucketAndObject()
	if dstBucket == "" {
		return probe.NewError(BucketNameEmpty{})
	}

	tokens := splitStr(source, string(c.targetURL.Separator), 3)

	// Source object
	src := minio.NewSourceInfo(tokens[1], tokens[2], srcSSE)

	// Destination object
	dst, e := minio.NewDestinationInfo(dstBucket, dstObject, tgtSSE, nil)
	if e != nil {
		return probe.NewError(e)
	}

	if e = c.api.ComposeObjectWithProgress(dst, []minio.SourceInfo{src}, progress); e != nil {
		errResponse := minio.ToErrorResponse(e)
		if errResponse.Code == "AccessDenied" {
			return probe.NewError(PathInsufficientPermission{
				Path: c.targetURL.String(),
			})
		}
		if errResponse.Code == "NoSuchBucket" {
			return probe.NewError(BucketDoesNotExist{
				Bucket: dstBucket,
			})
		}
		if errResponse.Code == "InvalidBucketName" {
			return probe.NewError(BucketInvalid{
				Bucket: dstBucket,
			})
		}
		if errResponse.Code == "NoSuchKey" {
			return probe.NewError(ObjectMissing{})
		}
		return probe.NewError(e)
	}
	return nil
}

// Put - upload an object with custom metadata.
func (c *s3Client) Put(ctx context.Context, reader io.Reader, size int64, metadata map[string]string, progress io.Reader, sse encrypt.ServerSide) (int64, *probe.Error) {
	bucket, object := c.url2BucketAndObject()
	contentType, ok := metadata["Content-Type"]
	if ok {
		delete(metadata, "Content-Type")
	} else {
		// Set content-type if not specified.
		contentType = "application/octet-stream"
	}

	cacheControl, ok := metadata["Cache-Control"]
	if ok {
		delete(metadata, "Cache-Control")
	}

	contentEncoding, ok := metadata["Content-Encoding"]
	if ok {
		delete(metadata, "Content-Encoding")
	}

	contentDisposition, ok := metadata["Content-Disposition"]
	if ok {
		delete(metadata, "Content-Disposition")
	}

	contentLanguage, ok := metadata["Content-Language"]
	if ok {
		delete(metadata, "Content-Language")
	}

	storageClass, ok := metadata["X-Amz-Storage-Class"]
	if ok {
		delete(metadata, "X-Amz-Storage-Class")
	}
	if bucket == "" {
		return 0, probe.NewError(BucketNameEmpty{})
	}
	opts := minio.PutObjectOptions{
		UserMetadata:         metadata,
		Progress:             progress,
		NumThreads:           defaultMultipartThreadsNum,
		ContentType:          contentType,
		CacheControl:         cacheControl,
		ContentDisposition:   contentDisposition,
		ContentEncoding:      contentEncoding,
		ContentLanguage:      contentLanguage,
		StorageClass:         strings.ToUpper(storageClass),
		ServerSideEncryption: sse,
	}
	n, e := c.api.PutObjectWithContext(ctx, bucket, object, reader, size, opts)
	if e != nil {
		errResponse := minio.ToErrorResponse(e)
		if errResponse.Code == "UnexpectedEOF" || e == io.EOF {
			return n, probe.NewError(UnexpectedEOF{
				TotalSize:    size,
				TotalWritten: n,
			})
		}
		if errResponse.Code == "AccessDenied" {
			return n, probe.NewError(PathInsufficientPermission{
				Path: c.targetURL.String(),
			})
		}
		if errResponse.Code == "MethodNotAllowed" {
			return n, probe.NewError(ObjectAlreadyExists{
				Object: object,
			})
		}
		if errResponse.Code == "XMinioObjectExistsAsDirectory" {
			return n, probe.NewError(ObjectAlreadyExistsAsDirectory{
				Object: object,
			})
		}
		if errResponse.Code == "NoSuchBucket" {
			return n, probe.NewError(BucketDoesNotExist{
				Bucket: bucket,
			})
		}
		if errResponse.Code == "InvalidBucketName" {
			return n, probe.NewError(BucketInvalid{
				Bucket: bucket,
			})
		}
		if errResponse.Code == "NoSuchKey" {
			return n, probe.NewError(ObjectMissing{})
		}
		return n, probe.NewError(e)
	}
	return n, nil
}

// Remove incomplete uploads.
func (c *s3Client) removeIncompleteObjects(bucket string, objectsCh <-chan string) <-chan minio.RemoveObjectError {
	removeObjectErrorCh := make(chan minio.RemoveObjectError)

	// Goroutine reads from objectsCh and sends error to removeObjectErrorCh if any.
	go func() {
		defer close(removeObjectErrorCh)

		for object := range objectsCh {
			if err := c.api.RemoveIncompleteUpload(bucket, object); err != nil {
				removeObjectErrorCh <- minio.RemoveObjectError{ObjectName: object, Err: err}
			}
		}
	}()

	return removeObjectErrorCh
}

// Remove - remove object or bucket(s).
func (c *s3Client) Remove(isIncomplete bool, contentCh <-chan *clientContent) <-chan *probe.Error {
	errorCh := make(chan *probe.Error)

	prevBucket := ""
	// Maintain objectsCh, statusCh for each bucket
	var objectsCh chan string
	var statusCh <-chan minio.RemoveObjectError

	isRemoveBucket := false

	go func() {
		defer close(errorCh)
		for content := range contentCh {
			// Convert content.URL.Path to objectName for objectsCh.
			bucket, objectName := c.splitPath(content.URL.Path)
			// Init objectsCh the first time.
			if prevBucket == "" {
				objectsCh = make(chan string)
				prevBucket = bucket
				if isIncomplete {
					statusCh = c.removeIncompleteObjects(bucket, objectsCh)
				} else {
					statusCh = c.api.RemoveObjects(bucket, objectsCh)
				}
			}

			if prevBucket != bucket {
				if objectsCh != nil {
					close(objectsCh)
				}
				for removeStatus := range statusCh {
					errorCh <- probe.NewError(removeStatus.Err)
				}
				// Remove bucket if it qualifies.
				if isRemoveBucket && !isIncomplete {
					if err := c.api.RemoveBucket(prevBucket); err != nil {
						errorCh <- probe.NewError(err)
					}
				}
				// Re-init objectsCh for next bucket
				isRemoveBucket = false
				objectsCh = make(chan string)
				if isIncomplete {
					statusCh = c.removeIncompleteObjects(bucket, objectsCh)
				} else {
					statusCh = c.api.RemoveObjects(bucket, objectsCh)
				}
				prevBucket = bucket
			}

			if objectName != "" {
				// Send object name once but continuously checks for pending
				// errors in parallel, the reason is that minio-go RemoveObjects
				// can block if there is any pending error not received yet.
				sent := false
				for !sent {
					select {
					case objectsCh <- objectName:
						sent = true
					case removeStatus := <-statusCh:
						errorCh <- probe.NewError(removeStatus.Err)
					}
				}
			} else {
				// end of bucket - close the objectsCh
				isRemoveBucket = true
				if objectsCh != nil {
					close(objectsCh)
				}
				objectsCh = nil
			}
		}
		// Close objectsCh at end of contentCh
		if objectsCh != nil {
			close(objectsCh)
		}
		// Write remove objects status to errorCh
		if statusCh != nil {
			for removeStatus := range statusCh {
				errorCh <- probe.NewError(removeStatus.Err)
			}
		}
		// Remove last bucket if it qualifies.
		if isRemoveBucket && !isIncomplete {
			if err := c.api.RemoveBucket(prevBucket); err != nil {
				errorCh <- probe.NewError(err)
			}
		}
	}()
	return errorCh
}

// MakeBucket - make a new bucket.
func (c *s3Client) MakeBucket(region string, ignoreExisting bool) *probe.Error {
	bucket, object := c.url2BucketAndObject()
	if bucket == "" {
		return probe.NewError(BucketNameEmpty{})
	}
	if object != "" {
		if strings.HasSuffix(object, "/") {
		retry:
			if _, e := c.api.PutObject(bucket, object, bytes.NewReader([]byte("")), 0, minio.PutObjectOptions{}); e != nil {
				switch minio.ToErrorResponse(e).Code {
				case "NoSuchBucket":
					e = c.api.MakeBucket(bucket, region)
					if e != nil {
						return probe.NewError(e)
					}
					goto retry
				}
				return probe.NewError(e)
			}
			return nil
		}
		return probe.NewError(BucketNameTopLevel{})
	}

	e := c.api.MakeBucket(bucket, region)
	if e != nil {
		// Ignore bucket already existing error when ignoreExisting flag is enabled
		if ignoreExisting {
			switch minio.ToErrorResponse(e).Code {
			case "BucketAlreadyOwnedByYou":
				fallthrough
			case "BucketAlreadyExists":
				return nil
			}
		}
		return probe.NewError(e)
	}
	return nil
}

// GetAccessRules - get configured policies from the server
func (c *s3Client) GetAccessRules() (map[string]string, *probe.Error) {
	bucket, object := c.url2BucketAndObject()
	if bucket == "" {
		return map[string]string{}, probe.NewError(BucketNameEmpty{})
	}
	policies := map[string]string{}
	policyStr, e := c.api.GetBucketPolicy(bucket)
	if e != nil {
		return nil, probe.NewError(e)
	}
	if policyStr == "" {
		return policies, nil
	}
	var p policy.BucketAccessPolicy
	if e = json.Unmarshal([]byte(policyStr), &p); e != nil {
		return nil, probe.NewError(e)
	}
	policyRules := policy.GetPolicies(p.Statements, bucket, object)
	// Hide policy data structure at this level
	for k, v := range policyRules {
		policies[k] = string(v)
	}
	return policies, nil
}

// GetAccess get access policy permissions.
func (c *s3Client) GetAccess() (string, *probe.Error) {
	bucket, object := c.url2BucketAndObject()
	if bucket == "" {
		return "", probe.NewError(BucketNameEmpty{})
	}
	policyStr, e := c.api.GetBucketPolicy(bucket)
	if e != nil {
		return "", probe.NewError(e)
	}
	if policyStr == "" {
		return string(policy.BucketPolicyNone), nil
	}
	var p policy.BucketAccessPolicy
	if e = json.Unmarshal([]byte(policyStr), &p); e != nil {
		return "", probe.NewError(e)
	}
	return string(policy.GetPolicy(p.Statements, bucket, object)), nil
}

// SetAccess set access policy permissions.
func (c *s3Client) SetAccess(bucketPolicy string) *probe.Error {
	bucket, object := c.url2BucketAndObject()
	if bucket == "" {
		return probe.NewError(BucketNameEmpty{})
	}
	policyStr, e := c.api.GetBucketPolicy(bucket)
	if e != nil {
		return probe.NewError(e)
	}
	var p = policy.BucketAccessPolicy{Version: "2012-10-17"}
	if policyStr != "" {
		if e = json.Unmarshal([]byte(policyStr), &p); e != nil {
			return probe.NewError(e)
		}
	}
	p.Statements = policy.SetPolicy(p.Statements, policy.BucketPolicy(bucketPolicy), bucket, object)
	if len(p.Statements) == 0 {
		if e = c.api.SetBucketPolicy(bucket, ""); e != nil {
			return probe.NewError(e)
		}
		return nil
	}
	policyB, e := json.Marshal(p)
	if e != nil {
		return probe.NewError(e)
	}
	if e = c.api.SetBucketPolicy(bucket, string(policyB)); e != nil {
		return probe.NewError(e)
	}
	return nil
}

// listObjectWrapper - select ObjectList version depending on the target hostname
func (c *s3Client) listObjectWrapper(bucket, object string, isRecursive bool, doneCh chan struct{}) <-chan minio.ObjectInfo {
	if isAmazon(c.targetURL.Host) || isAmazonAccelerated(c.targetURL.Host) {
		return c.api.ListObjectsV2(bucket, object, isRecursive, doneCh)
	}
	return c.api.ListObjects(bucket, object, isRecursive, doneCh)
}

// Stat - send a 'HEAD' on a bucket or object to fetch its metadata.
func (c *s3Client) Stat(isIncomplete, isFetchMeta bool, sse encrypt.ServerSide) (*clientContent, *probe.Error) {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	bucket, object := c.url2BucketAndObject()
	// Bucket name cannot be empty, stat on URL has no meaning.
	if bucket == "" {
		return nil, probe.NewError(BucketNameEmpty{})
	}

	if object == "" {
		exists, e := c.api.BucketExists(bucket)
		if e != nil {
			return nil, probe.NewError(e)
		}
		if !exists {
			return nil, probe.NewError(BucketDoesNotExist{Bucket: bucket})
		}
		bucketMetadata := c.bucketStat()
		return &bucketMetadata, nil
	}

	// The following code tries to calculate if a given prefix/object does really exist
	// using minio-go listing API. The following inputs are supported:
	//     - /path/to/existing/object
	//     - /path/to/existing_directory
	//     - /path/to/existing_directory/
	//     - /path/to/empty_directory
	//     - /path/to/empty_directory/

	nonRecursive := false
	objectMetadata := &clientContent{}

	// Prefix to pass to minio-go listing in order to fetch a given object/directory
	prefix := strings.TrimRight(object, string(c.targetURL.Separator))

	// If the request is for incomplete upload stat, handle it here.
	if isIncomplete {
		for objectMultipartInfo := range c.api.ListIncompleteUploads(bucket, prefix, nonRecursive, nil) {
			if objectMultipartInfo.Err != nil {
				return nil, probe.NewError(objectMultipartInfo.Err)
			}

			if objectMultipartInfo.Key == object {
				objectMetadata.URL = *c.targetURL
				objectMetadata.Time = objectMultipartInfo.Initiated
				objectMetadata.Size = objectMultipartInfo.Size
				objectMetadata.Type = os.FileMode(0664)
				objectMetadata.Metadata = map[string]string{}
				objectMetadata.EncryptionHeaders = map[string]string{}
				return objectMetadata, nil
			}

			if strings.HasSuffix(objectMultipartInfo.Key, string(c.targetURL.Separator)) {
				objectMetadata.URL = *c.targetURL
				objectMetadata.Type = os.ModeDir
				objectMetadata.Metadata = map[string]string{}
				objectMetadata.EncryptionHeaders = map[string]string{}

				return objectMetadata, nil
			}
		}
		return nil, probe.NewError(ObjectMissing{})
	}

	opts := minio.StatObjectOptions{}
	opts.ServerSideEncryption = sse

	for objectStat := range c.listObjectWrapper(bucket, prefix, nonRecursive, nil) {
		if objectStat.Err != nil {
			return nil, probe.NewError(objectStat.Err)
		}
		if strings.HasSuffix(objectStat.Key, string(c.targetURL.Separator)) {
			objectMetadata.URL = *c.targetURL
			objectMetadata.Type = os.ModeDir

			if isFetchMeta {
				stat, err := c.getObjectStat(bucket, object, opts)
				if err != nil {
					return nil, err
				}
				objectMetadata.ETag = stat.ETag
				objectMetadata.Metadata = stat.Metadata
				objectMetadata.EncryptionHeaders = stat.EncryptionHeaders
			}
			return objectMetadata, nil
		} else if objectStat.Key == object {
			objectMetadata.URL = *c.targetURL
			objectMetadata.Time = objectStat.LastModified
			objectMetadata.Size = objectStat.Size
			objectMetadata.ETag = objectStat.ETag
			objectMetadata.Type = os.FileMode(0664)
			objectMetadata.Metadata = map[string]string{}
			objectMetadata.EncryptionHeaders = map[string]string{}
			if isFetchMeta {
				stat, err := c.getObjectStat(bucket, object, opts)
				if err != nil {
					return nil, err
				}
				objectMetadata.Metadata = stat.Metadata
				objectMetadata.EncryptionHeaders = stat.EncryptionHeaders
			}
			return objectMetadata, nil
		}
	}
	return c.getObjectStat(bucket, object, opts)
}

// getObjectStat returns the metadata of an object from a HEAD call.
func (c *s3Client) getObjectStat(bucket, object string, opts minio.StatObjectOptions) (*clientContent, *probe.Error) {
	objectMetadata := &clientContent{}
	objectStat, e := c.api.StatObject(bucket, object, opts)
	if e != nil {
		errResponse := minio.ToErrorResponse(e)
		if errResponse.Code == "AccessDenied" {
			return nil, probe.NewError(PathInsufficientPermission{Path: c.targetURL.String()})
		}
		if errResponse.Code == "NoSuchBucket" {
			return nil, probe.NewError(BucketDoesNotExist{
				Bucket: bucket,
			})
		}
		if errResponse.Code == "InvalidBucketName" {
			return nil, probe.NewError(BucketInvalid{
				Bucket: bucket,
			})
		}
		if errResponse.Code == "NoSuchKey" {
			return nil, probe.NewError(ObjectMissing{})
		}
		return nil, probe.NewError(e)
	}
	objectMetadata.URL = *c.targetURL
	objectMetadata.Time = objectStat.LastModified
	objectMetadata.Size = objectStat.Size
	objectMetadata.ETag = objectStat.ETag
	objectMetadata.Type = os.FileMode(0664)
	objectMetadata.Metadata = map[string]string{}
	objectMetadata.EncryptionHeaders = map[string]string{}
	objectMetadata.Metadata["Content-Type"] = objectStat.ContentType
	for k, v := range objectStat.Metadata {
		isCSEHeader := false
		for _, header := range cseHeaders {
			if (strings.Compare(strings.ToLower(header), strings.ToLower(k)) == 0) ||
				strings.HasPrefix(strings.ToLower(serverEncryptionKeyPrefix), strings.ToLower(k)) {
				if len(v) > 0 {
					objectMetadata.EncryptionHeaders[k] = v[0]
				}
				isCSEHeader = true
				break
			}
		}
		if !isCSEHeader {
			if len(v) > 0 {
				objectMetadata.Metadata[k] = v[0]
			}
		}
	}
	objectMetadata.ETag = objectStat.ETag
	return objectMetadata, nil
}

func isAmazon(host string) bool {
	return s3utils.IsAmazonEndpoint(url.URL{Host: host})
}

func isAmazonChina(host string) bool {
	amazonS3ChinaHost := regexp.MustCompile(`^s3\.(cn.*?)\.amazonaws\.com\.cn$`)
	parts := amazonS3ChinaHost.FindStringSubmatch(host)
	return len(parts) > 1
}

func isAmazonAccelerated(host string) bool {
	return host == "s3-accelerate.amazonaws.com"
}

func isGoogle(host string) bool {
	return s3utils.IsGoogleEndpoint(url.URL{Host: host})
}

// Figure out if the URL is of 'virtual host' style.
// Use lookup from config to see if dns/path style look
// up should be used. If it is set to "auto", use virtual
// style for supported hosts such as Amazon S3 and Google
// Cloud Storage. Otherwise, default to path style
func isVirtualHostStyle(host string, lookup minio.BucketLookupType) bool {
	if lookup == minio.BucketLookupDNS {
		return true
	}
	if lookup == minio.BucketLookupPath {
		return false
	}
	return isAmazon(host) && !isAmazonChina(host) || isGoogle(host) || isAmazonAccelerated(host)
}

// url2BucketAndObject gives bucketName and objectName from URL path.
func (c *s3Client) url2BucketAndObject() (bucketName, objectName string) {
	path := c.targetURL.Path
	// Convert any virtual host styled requests.
	//
	// For the time being this check is introduced for S3,
	// If you have custom virtual styled hosts please.
	// List them below.
	if c.virtualStyle {
		var bucket string
		hostIndex := strings.Index(c.targetURL.Host, "s3")
		if hostIndex == -1 {
			hostIndex = strings.Index(c.targetURL.Host, "s3-accelerate")
		}
		if hostIndex == -1 {
			hostIndex = strings.Index(c.targetURL.Host, "storage.googleapis")
		}
		if hostIndex > 0 {
			bucket = c.targetURL.Host[:hostIndex-1]
			path = string(c.targetURL.Separator) + bucket + c.targetURL.Path
		}
	}
	tokens := splitStr(path, string(c.targetURL.Separator), 3)
	return tokens[1], tokens[2]
}

// splitPath split path into bucket and object.
func (c *s3Client) splitPath(path string) (bucketName, objectName string) {
	path = strings.TrimPrefix(path, string(c.targetURL.Separator))

	// Handle path if its virtual style.
	if c.virtualStyle {
		hostIndex := strings.Index(c.targetURL.Host, "s3")
		if hostIndex == -1 {
			hostIndex = strings.Index(c.targetURL.Host, "s3-accelerate")
		}
		if hostIndex == -1 {
			hostIndex = strings.Index(c.targetURL.Host, "storage.googleapis")
		}
		if hostIndex > 0 {
			bucketName = c.targetURL.Host[:hostIndex-1]
			objectName = path
			return bucketName, objectName
		}
	}

	tokens := splitStr(path, string(c.targetURL.Separator), 2)
	return tokens[0], tokens[1]
}

/// Bucket API operations.

// List - list at delimited path, if not recursive.
func (c *s3Client) List(isRecursive, isIncomplete bool, showDir DirOpt) <-chan *clientContent {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	contentCh := make(chan *clientContent)
	if isIncomplete {
		if isRecursive {
			if showDir == DirNone {
				go c.listIncompleteRecursiveInRoutine(contentCh)
			} else {
				go c.listIncompleteRecursiveInRoutineDirOpt(contentCh, showDir)
			}
		} else {
			go c.listIncompleteInRoutine(contentCh)
		}
	} else {
		if isRecursive {
			if showDir == DirNone {
				go c.listRecursiveInRoutine(contentCh)
			} else {
				go c.listRecursiveInRoutineDirOpt(contentCh, showDir)
			}
		} else {
			go c.listInRoutine(contentCh)
		}
	}

	return contentCh
}

func (c *s3Client) listIncompleteInRoutine(contentCh chan *clientContent) {
	defer close(contentCh)
	// get bucket and object from URL.
	b, o := c.url2BucketAndObject()
	switch {
	case b == "" && o == "":
		buckets, err := c.api.ListBuckets()
		if err != nil {
			contentCh <- &clientContent{
				Err: probe.NewError(err),
			}
			return
		}
		isRecursive := false
		for _, bucket := range buckets {
			for object := range c.api.ListIncompleteUploads(bucket.Name, o, isRecursive, nil) {
				if object.Err != nil {
					contentCh <- &clientContent{
						Err: probe.NewError(object.Err),
					}
					continue
				}
				content := &clientContent{}
				url := *c.targetURL
				// Join bucket with - incoming object key.
				url.Path = c.joinPath(bucket.Name, object.Key)
				switch {
				case strings.HasSuffix(object.Key, string(c.targetURL.Separator)):
					// We need to keep the trailing Separator, do not use filepath.Join().
					content.URL = url
					content.Time = time.Now()
					content.Type = os.ModeDir
				default:
					content.URL = url
					content.Size = object.Size
					content.Time = object.Initiated
					content.Type = os.ModeTemporary
				}
				contentCh <- content
			}
		}
	default:
		isRecursive := false
		for object := range c.api.ListIncompleteUploads(b, o, isRecursive, nil) {
			if object.Err != nil {
				contentCh <- &clientContent{
					Err: probe.NewError(object.Err),
				}
				continue
			}
			content := &clientContent{}
			url := *c.targetURL
			// Join bucket with - incoming object key.
			url.Path = c.joinPath(b, object.Key)
			switch {
			case strings.HasSuffix(object.Key, string(c.targetURL.Separator)):
				// We need to keep the trailing Separator, do not use filepath.Join().
				content.URL = url
				content.Time = time.Now()
				content.Type = os.ModeDir
			default:
				content.URL = url
				content.Size = object.Size
				content.Time = object.Initiated
				content.Type = os.ModeTemporary
			}
			contentCh <- content
		}
	}
}

func (c *s3Client) listIncompleteRecursiveInRoutine(contentCh chan *clientContent) {
	defer close(contentCh)
	// get bucket and object from URL.
	b, o := c.url2BucketAndObject()
	switch {
	case b == "" && o == "":
		buckets, err := c.api.ListBuckets()
		if err != nil {
			contentCh <- &clientContent{
				Err: probe.NewError(err),
			}
			return
		}
		isRecursive := true
		for _, bucket := range buckets {
			for object := range c.api.ListIncompleteUploads(bucket.Name, o, isRecursive, nil) {
				if object.Err != nil {
					contentCh <- &clientContent{
						Err: probe.NewError(object.Err),
					}
					continue
				}
				url := *c.targetURL
				url.Path = c.joinPath(bucket.Name, object.Key)
				content := &clientContent{}
				content.URL = url
				content.Size = object.Size
				content.Time = object.Initiated
				content.Type = os.ModeTemporary
				contentCh <- content
			}
		}
	default:
		isRecursive := true
		for object := range c.api.ListIncompleteUploads(b, o, isRecursive, nil) {
			if object.Err != nil {
				contentCh <- &clientContent{
					Err: probe.NewError(object.Err),
				}
				continue
			}
			url := *c.targetURL
			// Join bucket and incoming object key.
			url.Path = c.joinPath(b, object.Key)
			content := &clientContent{}
			content.URL = url
			content.Size = object.Size
			content.Time = object.Initiated
			content.Type = os.ModeTemporary
			contentCh <- content
		}
	}
}

// Convert objectMultipartInfo to clientContent
func (c *s3Client) objectMultipartInfo2ClientContent(bucket string, entry minio.ObjectMultipartInfo) clientContent {

	content := clientContent{}
	url := *c.targetURL
	// Join bucket and incoming object key.
	url.Path = c.joinPath(bucket, entry.Key)
	content.URL = url
	content.Size = entry.Size
	content.Time = entry.Initiated

	if strings.HasSuffix(entry.Key, "/") {
		content.Type = os.ModeDir
	} else {
		content.Type = os.ModeTemporary
	}

	return content
}

// Recursively lists incomplete uploads.
func (c *s3Client) listIncompleteRecursiveInRoutineDirOpt(contentCh chan *clientContent, dirOpt DirOpt) {
	defer close(contentCh)

	// Closure function reads list of incomplete uploads and sends to contentCh. If a directory is found, it lists
	// incomplete uploads of the directory content recursively.
	var listDir func(bucket, object string) bool
	listDir = func(bucket, object string) (isStop bool) {
		isRecursive := false
		for entry := range c.api.ListIncompleteUploads(bucket, object, isRecursive, nil) {
			if entry.Err != nil {
				url := *c.targetURL
				url.Path = c.joinPath(bucket, object)
				contentCh <- &clientContent{URL: url, Err: probe.NewError(entry.Err)}

				errResponse := minio.ToErrorResponse(entry.Err)
				if errResponse.Code == "AccessDenied" {
					continue
				}

				return true
			}

			content := c.objectMultipartInfo2ClientContent(bucket, entry)

			// Handle if object.Key is a directory.
			if strings.HasSuffix(entry.Key, string(c.targetURL.Separator)) {
				if dirOpt == DirFirst {
					contentCh <- &content
				}
				if listDir(bucket, entry.Key) {
					return true
				}
				if dirOpt == DirLast {
					contentCh <- &content
				}
			} else {
				contentCh <- &content
			}
		}

		return false
	}

	bucket, object := c.url2BucketAndObject()
	var cContent *clientContent
	var buckets []minio.BucketInfo
	var allBuckets bool
	// List all buckets if bucket and object are empty.
	if bucket == "" && object == "" {
		var err error
		allBuckets = true
		buckets, err = c.api.ListBuckets()
		if err != nil {
			contentCh <- &clientContent{URL: *c.targetURL, Err: probe.NewError(err)}
		}
	} else if object == "" {
		// Get bucket stat if object is empty.
		content := c.bucketStat()
		cContent = &content
		if content.Err != nil {
			contentCh <- cContent
			return
		}
		buckets = append(buckets, minio.BucketInfo{Name: bucket, CreationDate: content.Time})
	} else if strings.HasSuffix(object, string(c.targetURL.Separator)) {
		// Get stat of given object is a directory.
		isIncomplete := true
		content, perr := c.Stat(isIncomplete, false, nil)
		cContent = content
		if perr != nil {
			contentCh <- &clientContent{URL: *c.targetURL, Err: perr}
			return
		}
		buckets = append(buckets, minio.BucketInfo{Name: bucket, CreationDate: content.Time})
	}
	for _, bucket := range buckets {
		if allBuckets {
			url := *c.targetURL
			url.Path = c.joinPath(bucket.Name)
			cContent = &clientContent{
				URL:  url,
				Time: bucket.CreationDate,
				Type: os.ModeDir,
			}
		}
		if cContent != nil && dirOpt == DirFirst {
			contentCh <- cContent
		}
		//Recursively push all object prefixes into contentCh to mimic directory listing
		listDir(bucket.Name, object)

		if cContent != nil && dirOpt == DirLast {
			contentCh <- cContent
		}
	}
}

// Returns new path by joining path segments with URL path separator.
func (c *s3Client) joinPath(bucket string, objects ...string) string {
	p := string(c.targetURL.Separator) + bucket
	for _, o := range objects {
		p += string(c.targetURL.Separator) + o
	}
	return p
}

// Convert objectInfo to clientContent
func (c *s3Client) objectInfo2ClientContent(bucket string, entry minio.ObjectInfo) clientContent {

	content := clientContent{}
	url := *c.targetURL
	// Join bucket and incoming object key.
	url.Path = c.joinPath(bucket, entry.Key)
	content.URL = url
	content.Size = entry.Size
	content.ETag = entry.ETag
	content.Time = entry.LastModified

	if strings.HasSuffix(entry.Key, "/") && entry.Size == 0 && entry.LastModified.IsZero() {
		content.Type = os.ModeDir
	} else {
		content.Type = os.FileMode(0664)
	}

	return content
}

// Returns bucket stat info of current bucket.
func (c *s3Client) bucketStat() clientContent {
	bucketName, _ := c.url2BucketAndObject()

	buckets, err := c.api.ListBuckets()
	if err != nil {
		return clientContent{Err: probe.NewError(err)}
	}

	for _, bucket := range buckets {
		if bucket.Name == bucketName {
			return clientContent{URL: *c.targetURL, Time: bucket.CreationDate, Type: os.ModeDir}
		}
	}

	return clientContent{Err: probe.NewError(BucketDoesNotExist{Bucket: bucketName})}
}

// Recursively lists objects.
func (c *s3Client) listRecursiveInRoutineDirOpt(contentCh chan *clientContent, dirOpt DirOpt) {
	defer close(contentCh)
	// Closure function reads list objects and sends to contentCh. If a directory is found, it lists
	// objects of the directory content recursively.
	var listDir func(bucket, object string) bool
	listDir = func(bucket, object string) (isStop bool) {
		isRecursive := false
		for entry := range c.listObjectWrapper(bucket, object, isRecursive, nil) {
			if entry.Err != nil {
				url := *c.targetURL
				url.Path = c.joinPath(bucket, object)
				contentCh <- &clientContent{URL: url, Err: probe.NewError(entry.Err)}

				errResponse := minio.ToErrorResponse(entry.Err)
				if errResponse.Code == "AccessDenied" {
					continue
				}
				return true
			}

			content := c.objectInfo2ClientContent(bucket, entry)

			// Handle if object.Key is a directory.
			if content.Type.IsDir() {
				if dirOpt == DirFirst {
					contentCh <- &content
				}
				if listDir(bucket, entry.Key) {
					return true
				}
				if dirOpt == DirLast {
					contentCh <- &content
				}
			} else {
				contentCh <- &content
			}
		}
		return false
	}

	bucket, object := c.url2BucketAndObject()

	var cContent *clientContent
	var buckets []minio.BucketInfo
	var allBuckets bool
	// List all buckets if bucket and object are empty.
	if bucket == "" && object == "" {
		var err error
		allBuckets = true
		buckets, err = c.api.ListBuckets()
		if err != nil {
			contentCh <- &clientContent{URL: *c.targetURL, Err: probe.NewError(err)}
		}
	} else if object == "" {
		// Get bucket stat if object is empty.
		content := c.bucketStat()
		cContent = &content
		if content.Err != nil {
			contentCh <- cContent
			return
		}
		buckets = append(buckets, minio.BucketInfo{Name: bucket, CreationDate: content.Time})
	} else {
		// Get stat of given object is a directory.
		isIncomplete := false
		isFetchMeta := false
		content, perr := c.Stat(isIncomplete, isFetchMeta, nil)
		cContent = content
		if perr != nil {
			contentCh <- &clientContent{URL: *c.targetURL, Err: perr}
			return
		}
		buckets = append(buckets, minio.BucketInfo{Name: bucket, CreationDate: content.Time})
	}

	for _, bucket := range buckets {
		if allBuckets {
			url := *c.targetURL
			url.Path = c.joinPath(bucket.Name)
			cContent = &clientContent{
				URL:  url,
				Time: bucket.CreationDate,
				Type: os.ModeDir,
			}
		}
		if cContent != nil && dirOpt == DirFirst {
			contentCh <- cContent
		}
		// Recurse thru prefixes to mimic directory listing and push into contentCh
		listDir(bucket.Name, object)

		if cContent != nil && dirOpt == DirLast {
			contentCh <- cContent
		}
	}
}

func (c *s3Client) listInRoutine(contentCh chan *clientContent) {
	defer close(contentCh)
	// get bucket and object from URL.
	b, o := c.url2BucketAndObject()
	switch {
	case b == "" && o == "":
		buckets, e := c.api.ListBuckets()
		if e != nil {
			contentCh <- &clientContent{
				Err: probe.NewError(e),
			}
			return
		}
		for _, bucket := range buckets {
			url := *c.targetURL
			url.Path = c.joinPath(bucket.Name)
			content := &clientContent{}
			content.URL = url
			content.Size = 0
			content.Time = bucket.CreationDate
			content.Type = os.ModeDir
			contentCh <- content
		}
	case b != "" && !strings.HasSuffix(c.targetURL.Path, string(c.targetURL.Separator)) && o == "":
		buckets, e := c.api.ListBuckets()
		if e != nil {
			contentCh <- &clientContent{
				Err: probe.NewError(e),
			}
		}
		for _, bucket := range buckets {
			if bucket.Name == b {
				content := &clientContent{}
				content.URL = *c.targetURL
				content.Size = 0
				content.Time = bucket.CreationDate
				content.Type = os.ModeDir
				contentCh <- content
				break
			}
		}
	default:
		isRecursive := false
		for object := range c.listObjectWrapper(b, o, isRecursive, nil) {
			if object.Err != nil {
				contentCh <- &clientContent{
					Err: probe.NewError(object.Err),
				}
				return
			}

			// Avoid sending an empty directory when we are specifically listing it
			if strings.HasSuffix(object.Key, string(c.targetURL.Separator)) && o == object.Key {
				continue
			}

			content := &clientContent{}
			url := *c.targetURL
			// Join bucket and incoming object key.
			url.Path = c.joinPath(b, object.Key)
			switch {
			case strings.HasSuffix(object.Key, string(c.targetURL.Separator)):
				// We need to keep the trailing Separator, do not use filepath.Join().
				content.URL = url
				content.Time = time.Now()
				content.Type = os.ModeDir
			default:
				content.URL = url
				content.Size = object.Size
				content.ETag = object.ETag
				content.Time = object.LastModified
				content.Type = os.FileMode(0664)
			}
			contentCh <- content
		}
	}
}

// S3 offers a range of storage classes designed for
// different use cases, following list captures these.
const (
	// General purpose.
	// s3StorageClassStandard = "STANDARD"
	// Infrequent access.
	// s3StorageClassInfrequent = "STANDARD_IA"
	// Reduced redundancy access.
	// s3StorageClassRedundancy = "REDUCED_REDUNDANCY"
	// Archive access.
	s3StorageClassGlacier = "GLACIER"
)

func (c *s3Client) listRecursiveInRoutine(contentCh chan *clientContent) {
	defer close(contentCh)
	// get bucket and object from URL.
	b, o := c.url2BucketAndObject()
	switch {
	case b == "" && o == "":
		buckets, err := c.api.ListBuckets()
		if err != nil {
			contentCh <- &clientContent{
				Err: probe.NewError(err),
			}
			return
		}
		for _, bucket := range buckets {
			isRecursive := true
			for object := range c.listObjectWrapper(bucket.Name, o, isRecursive, nil) {
				// Return error if we encountered glacier object and continue.
				if object.StorageClass == s3StorageClassGlacier {
					contentCh <- &clientContent{
						Err: probe.NewError(ObjectOnGlacier{object.Key}),
					}
					continue
				}
				if object.Err != nil {
					contentCh <- &clientContent{
						Err: probe.NewError(object.Err),
					}
					continue
				}
				content := &clientContent{}
				objectURL := *c.targetURL
				objectURL.Path = c.joinPath(bucket.Name, object.Key)
				content.URL = objectURL
				content.Size = object.Size
				content.ETag = object.ETag
				content.Time = object.LastModified
				content.Type = os.FileMode(0664)
				contentCh <- content
			}
		}
	default:
		isRecursive := true
		for object := range c.listObjectWrapper(b, o, isRecursive, nil) {
			if object.Err != nil {
				contentCh <- &clientContent{
					Err: probe.NewError(object.Err),
				}
				continue
			}
			content := &clientContent{}
			url := *c.targetURL
			// Join bucket and incoming object key.
			url.Path = c.joinPath(b, object.Key)
			content.URL = url
			content.Size = object.Size
			content.ETag = object.ETag
			content.Time = object.LastModified
			content.Type = os.FileMode(0664)
			contentCh <- content
		}
	}
}

// ShareDownload - get a usable presigned object url to share.
func (c *s3Client) ShareDownload(expires time.Duration) (string, *probe.Error) {
	bucket, object := c.url2BucketAndObject()
	// No additional request parameters are set for the time being.
	reqParams := make(url.Values)
	presignedURL, e := c.api.PresignedGetObject(bucket, object, expires, reqParams)
	if e != nil {
		return "", probe.NewError(e)
	}
	return presignedURL.String(), nil
}

// ShareUpload - get data for presigned post http form upload.
func (c *s3Client) ShareUpload(isRecursive bool, expires time.Duration, contentType string) (string, map[string]string, *probe.Error) {
	bucket, object := c.url2BucketAndObject()
	p := minio.NewPostPolicy()
	if e := p.SetExpires(UTCNow().Add(expires)); e != nil {
		return "", nil, probe.NewError(e)
	}
	if strings.TrimSpace(contentType) != "" || contentType != "" {
		// No need to verify for error here, since we have stripped out spaces.
		p.SetContentType(contentType)
	}
	if e := p.SetBucket(bucket); e != nil {
		return "", nil, probe.NewError(e)
	}
	if isRecursive {
		if e := p.SetKeyStartsWith(object); e != nil {
			return "", nil, probe.NewError(e)
		}
	} else {
		if e := p.SetKey(object); e != nil {
			return "", nil, probe.NewError(e)
		}
	}
	u, m, e := c.api.PresignedPostPolicy(p)
	if e != nil {
		return "", nil, probe.NewError(e)
	}
	return u.String(), m, nil
}
