1
0
mirror of https://github.com/minio/mc.git synced 2025-11-12 01:02:26 +03:00
Files
mc/cmd/client-s3.go
Harshavardhana 19e14db50a Avoid ListBuckets to keep Stat() ops on bucket simpler (#2699)
Due to multi-user feature on Minio and mc adoption in
many different restrictive environments, most users disable
access to ListBuckets() calls. We can avoid such network
operations as they only add marginal value.
2019-02-28 14:23:47 -08:00

1845 lines
51 KiB
Go

/*
* 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"
minio "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"
"golang.org/x/net/http2"
)
// 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)
}
tr := &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,
// 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 useTLS {
// Keep TLS config.
tlsConfig := &tls.Config{
RootCAs: globalRootCAs,
// Can't use SSLv3 because of POODLE and BEAST
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
}
if config.Insecure {
tlsConfig.InsecureSkipVerify = true
}
tr.TLSClientConfig = tlsConfig
// Because we create a custom TLSClientConfig, we have to opt-in to HTTP/2.
// See https://github.com/golang/go/issues/14275
if e = http2.ConfigureTransport(tr); e != nil {
return nil, probe.NewError(e)
}
}
var transport http.RoundTripper = tr
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 the new 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, isRemoveBucket 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
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
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
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, isJSON bool) *probe.Error {
bucket, object := c.url2BucketAndObject()
if bucket == "" {
return probe.NewError(BucketNameEmpty{})
}
if isJSON {
if e := c.api.SetBucketPolicy(bucket, bucketPolicy); e != nil {
return probe.NewError(e)
}
return nil
}
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 == "" {
content, err := c.bucketStat(bucket)
if err != nil {
return nil, err.Trace(bucket)
}
return content, 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 e error
allBuckets = true
buckets, e = c.api.ListBuckets()
if e != nil {
contentCh <- &clientContent{Err: probe.NewError(e)}
return
}
} else if object == "" {
// Get bucket stat if object is empty.
content, err := c.bucketStat(bucket)
if err != nil {
contentCh <- &clientContent{Err: err.Trace(bucket)}
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{Err: perr.Trace(bucket)}
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(bucket string) (*clientContent, *probe.Error) {
exists, e := c.api.BucketExists(bucket)
if e != nil {
return nil, probe.NewError(e)
}
if !exists {
return nil, probe.NewError(BucketDoesNotExist{Bucket: bucket})
}
return &clientContent{URL: *c.targetURL, Time: time.Unix(0, 0), Type: os.ModeDir}, nil
}
// 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 e error
allBuckets = true
buckets, e = c.api.ListBuckets()
if e != nil {
contentCh <- &clientContent{Err: probe.NewError(e)}
return
}
} else if object == "" {
// Get bucket stat if object is empty.
content, err := c.bucketStat(bucket)
if err != nil {
contentCh <- &clientContent{Err: err.Trace(bucket)}
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{Err: perr.Trace(bucket)}
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 == "":
content, err := c.bucketStat(b)
if err != nil {
contentCh <- &clientContent{Err: err.Trace(b)}
return
}
contentCh <- content
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
}