github.com/minio/console@v1.4.1/api/user_objects.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	b64 "encoding/base64"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"net/url"
    28  	"path/filepath"
    29  	"regexp"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/minio/minio-go/v7"
    35  
    36  	"github.com/minio/console/pkg/utils"
    37  
    38  	"github.com/go-openapi/runtime"
    39  	"github.com/go-openapi/runtime/middleware"
    40  	"github.com/klauspost/compress/zip"
    41  	"github.com/minio/console/api/operations"
    42  	objectApi "github.com/minio/console/api/operations/object"
    43  	"github.com/minio/console/models"
    44  	mc "github.com/minio/mc/cmd"
    45  	"github.com/minio/mc/pkg/probe"
    46  	"github.com/minio/minio-go/v7/pkg/tags"
    47  	"github.com/minio/pkg/v3/mimedb"
    48  )
    49  
    50  // enum types
    51  const (
    52  	objectStorage = iota // MinIO and S3 compatible cloud storage
    53  	fileSystem           // POSIX compatible file systems
    54  )
    55  
    56  func registerObjectsHandlers(api *operations.ConsoleAPI) {
    57  	// list objects
    58  	api.ObjectListObjectsHandler = objectApi.ListObjectsHandlerFunc(func(params objectApi.ListObjectsParams, session *models.Principal) middleware.Responder {
    59  		resp, err := getListObjectsResponse(session, params)
    60  		if err != nil {
    61  			return objectApi.NewListObjectsDefault(err.Code).WithPayload(err.APIError)
    62  		}
    63  		return objectApi.NewListObjectsOK().WithPayload(resp)
    64  	})
    65  	// delete object
    66  	api.ObjectDeleteObjectHandler = objectApi.DeleteObjectHandlerFunc(func(params objectApi.DeleteObjectParams, session *models.Principal) middleware.Responder {
    67  		if err := getDeleteObjectResponse(session, params); err != nil {
    68  			return objectApi.NewDeleteObjectDefault(err.Code).WithPayload(err.APIError)
    69  		}
    70  		return objectApi.NewDeleteObjectOK()
    71  	})
    72  	// delete multiple objects
    73  	api.ObjectDeleteMultipleObjectsHandler = objectApi.DeleteMultipleObjectsHandlerFunc(func(params objectApi.DeleteMultipleObjectsParams, session *models.Principal) middleware.Responder {
    74  		if err := getDeleteMultiplePathsResponse(session, params); err != nil {
    75  			return objectApi.NewDeleteMultipleObjectsDefault(err.Code).WithPayload(err.APIError)
    76  		}
    77  		return objectApi.NewDeleteMultipleObjectsOK()
    78  	})
    79  	// download object
    80  	api.ObjectDownloadObjectHandler = objectApi.DownloadObjectHandlerFunc(func(params objectApi.DownloadObjectParams, session *models.Principal) middleware.Responder {
    81  		isFolder := false
    82  		ctx := params.HTTPRequest.Context()
    83  		var prefix string
    84  		if params.Prefix != "" {
    85  			encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
    86  			decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
    87  			if err != nil {
    88  				apiErr := ErrorWithContext(ctx, err)
    89  				return objectApi.NewDownloadObjectDefault(400).WithPayload(apiErr.APIError)
    90  			}
    91  			prefix = string(decodedPrefix)
    92  		}
    93  
    94  		folders := strings.Split(prefix, "/")
    95  		if folders[len(folders)-1] == "" {
    96  			isFolder = true
    97  		}
    98  		var resp middleware.Responder
    99  		var err *CodedAPIError
   100  
   101  		if isFolder {
   102  			resp, err = getDownloadFolderResponse(session, params)
   103  		} else {
   104  			resp, err = getDownloadObjectResponse(session, params)
   105  		}
   106  
   107  		if err != nil {
   108  			return objectApi.NewDownloadObjectDefault(err.Code).WithPayload(err.APIError)
   109  		}
   110  		return resp
   111  	})
   112  	// download multiple objects
   113  	api.ObjectDownloadMultipleObjectsHandler = objectApi.DownloadMultipleObjectsHandlerFunc(func(params objectApi.DownloadMultipleObjectsParams, session *models.Principal) middleware.Responder {
   114  		ctx := params.HTTPRequest.Context()
   115  		if len(params.ObjectList) < 1 {
   116  			errCode := ErrorWithContext(ctx, errors.New("could not download, since object list is empty"))
   117  			return objectApi.NewDownloadMultipleObjectsDefault(errCode.Code).WithPayload(errCode.APIError)
   118  		}
   119  		var resp middleware.Responder
   120  		var err *CodedAPIError
   121  		resp, err = getMultipleFilesDownloadResponse(session, params)
   122  		if err != nil {
   123  			return objectApi.NewDownloadMultipleObjectsDefault(err.Code).WithPayload(err.APIError)
   124  		}
   125  		return resp
   126  	})
   127  
   128  	// upload object
   129  	api.ObjectPostBucketsBucketNameObjectsUploadHandler = objectApi.PostBucketsBucketNameObjectsUploadHandlerFunc(func(params objectApi.PostBucketsBucketNameObjectsUploadParams, session *models.Principal) middleware.Responder {
   130  		if err := getUploadObjectResponse(session, params); err != nil {
   131  			if strings.Contains(err.APIError.DetailedMessage, "413") {
   132  				return objectApi.NewPostBucketsBucketNameObjectsUploadDefault(413).WithPayload(err.APIError)
   133  			}
   134  			return objectApi.NewPostBucketsBucketNameObjectsUploadDefault(err.Code).WithPayload(err.APIError)
   135  		}
   136  		return objectApi.NewPostBucketsBucketNameObjectsUploadOK()
   137  	})
   138  	// get share object url
   139  	api.ObjectShareObjectHandler = objectApi.ShareObjectHandlerFunc(func(params objectApi.ShareObjectParams, session *models.Principal) middleware.Responder {
   140  		resp, err := getShareObjectResponse(session, params)
   141  		if err != nil {
   142  			return objectApi.NewShareObjectDefault(err.Code).WithPayload(err.APIError)
   143  		}
   144  		return objectApi.NewShareObjectOK().WithPayload(*resp)
   145  	})
   146  	// set object legalhold status
   147  	api.ObjectPutObjectLegalHoldHandler = objectApi.PutObjectLegalHoldHandlerFunc(func(params objectApi.PutObjectLegalHoldParams, session *models.Principal) middleware.Responder {
   148  		if err := getSetObjectLegalHoldResponse(session, params); err != nil {
   149  			return objectApi.NewPutObjectLegalHoldDefault(err.Code).WithPayload(err.APIError)
   150  		}
   151  		return objectApi.NewPutObjectLegalHoldOK()
   152  	})
   153  	// set object retention
   154  	api.ObjectPutObjectRetentionHandler = objectApi.PutObjectRetentionHandlerFunc(func(params objectApi.PutObjectRetentionParams, session *models.Principal) middleware.Responder {
   155  		if err := getSetObjectRetentionResponse(session, params); err != nil {
   156  			return objectApi.NewPutObjectRetentionDefault(err.Code).WithPayload(err.APIError)
   157  		}
   158  		return objectApi.NewPutObjectRetentionOK()
   159  	})
   160  	// delete object retention
   161  	api.ObjectDeleteObjectRetentionHandler = objectApi.DeleteObjectRetentionHandlerFunc(func(params objectApi.DeleteObjectRetentionParams, session *models.Principal) middleware.Responder {
   162  		if err := deleteObjectRetentionResponse(session, params); err != nil {
   163  			return objectApi.NewDeleteObjectRetentionDefault(err.Code).WithPayload(err.APIError)
   164  		}
   165  		return objectApi.NewDeleteObjectRetentionOK()
   166  	})
   167  	// set tags in object
   168  	api.ObjectPutObjectTagsHandler = objectApi.PutObjectTagsHandlerFunc(func(params objectApi.PutObjectTagsParams, session *models.Principal) middleware.Responder {
   169  		if err := getPutObjectTagsResponse(session, params); err != nil {
   170  			return objectApi.NewPutObjectTagsDefault(err.Code).WithPayload(err.APIError)
   171  		}
   172  		return objectApi.NewPutObjectTagsOK()
   173  	})
   174  	// Restore file version
   175  	api.ObjectPutObjectRestoreHandler = objectApi.PutObjectRestoreHandlerFunc(func(params objectApi.PutObjectRestoreParams, session *models.Principal) middleware.Responder {
   176  		if err := getPutObjectRestoreResponse(session, params); err != nil {
   177  			return objectApi.NewPutObjectRestoreDefault(err.Code).WithPayload(err.APIError)
   178  		}
   179  		return objectApi.NewPutObjectRestoreOK()
   180  	})
   181  	// Metadata in object
   182  	api.ObjectGetObjectMetadataHandler = objectApi.GetObjectMetadataHandlerFunc(func(params objectApi.GetObjectMetadataParams, session *models.Principal) middleware.Responder {
   183  		resp, err := getObjectMetadataResponse(session, params)
   184  		if err != nil {
   185  			return objectApi.NewGetObjectMetadataDefault(err.Code).WithPayload(err.APIError)
   186  		}
   187  		return objectApi.NewGetObjectMetadataOK().WithPayload(resp)
   188  	})
   189  }
   190  
   191  // getListObjectsResponse returns a list of objects
   192  func getListObjectsResponse(session *models.Principal, params objectApi.ListObjectsParams) (*models.ListObjectsResponse, *CodedAPIError) {
   193  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   194  	defer cancel()
   195  	var prefix string
   196  	var recursive bool
   197  	var withVersions bool
   198  	var withMetadata bool
   199  	if params.Prefix != nil {
   200  		encodedPrefix := SanitizeEncodedPrefix(*params.Prefix)
   201  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
   202  		if err != nil {
   203  			return nil, ErrorWithContext(ctx, err)
   204  		}
   205  		prefix = string(decodedPrefix)
   206  	}
   207  	if params.Recursive != nil {
   208  		recursive = *params.Recursive
   209  	}
   210  	if params.WithVersions != nil {
   211  		withVersions = *params.WithVersions
   212  	}
   213  	if params.WithMetadata != nil {
   214  		withMetadata = *params.WithMetadata
   215  	}
   216  	// bucket request needed to proceed
   217  	if params.BucketName == "" {
   218  		return nil, ErrorWithContext(ctx, ErrBucketNameNotInRequest)
   219  	}
   220  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   221  	if err != nil {
   222  		return nil, ErrorWithContext(ctx, err)
   223  	}
   224  	// create a minioClient interface implementation
   225  	// defining the client to be used
   226  	minioClient := minioClient{client: mClient}
   227  
   228  	objs, err := listBucketObjects(ListObjectsOpts{
   229  		ctx:          ctx,
   230  		client:       minioClient,
   231  		bucketName:   params.BucketName,
   232  		prefix:       prefix,
   233  		recursive:    recursive,
   234  		withVersions: withVersions,
   235  		withMetadata: withMetadata,
   236  		limit:        params.Limit,
   237  	})
   238  	if err != nil {
   239  		return nil, ErrorWithContext(ctx, err)
   240  	}
   241  
   242  	resp := &models.ListObjectsResponse{
   243  		Objects: objs,
   244  		Total:   int64(len(objs)),
   245  	}
   246  	return resp, nil
   247  }
   248  
   249  type ListObjectsOpts struct {
   250  	ctx          context.Context
   251  	client       MinioClient
   252  	bucketName   string
   253  	prefix       string
   254  	recursive    bool
   255  	withVersions bool
   256  	withMetadata bool
   257  	limit        *int32
   258  }
   259  
   260  // listBucketObjects gets an array of objects in a bucket
   261  func listBucketObjects(listOpts ListObjectsOpts) ([]*models.BucketObject, error) {
   262  	var objects []*models.BucketObject
   263  	opts := minio.ListObjectsOptions{
   264  		Prefix:       listOpts.prefix,
   265  		Recursive:    listOpts.recursive,
   266  		WithVersions: listOpts.withVersions,
   267  		WithMetadata: listOpts.withMetadata,
   268  		MaxKeys:      100,
   269  	}
   270  	if listOpts.withMetadata {
   271  		opts.MaxKeys = 1
   272  	}
   273  	if listOpts.limit != nil {
   274  		opts.MaxKeys = int(*listOpts.limit)
   275  	}
   276  	var totalObjs int32
   277  	for lsObj := range listOpts.client.listObjects(listOpts.ctx, listOpts.bucketName, opts) {
   278  		if lsObj.Err != nil {
   279  			return nil, lsObj.Err
   280  		}
   281  
   282  		obj := &models.BucketObject{
   283  			Name:           lsObj.Key,
   284  			Size:           lsObj.Size,
   285  			LastModified:   lsObj.LastModified.Format(time.RFC3339),
   286  			ContentType:    lsObj.ContentType,
   287  			VersionID:      lsObj.VersionID,
   288  			IsLatest:       lsObj.IsLatest,
   289  			IsDeleteMarker: lsObj.IsDeleteMarker,
   290  			UserTags:       lsObj.UserTags,
   291  			UserMetadata:   lsObj.UserMetadata,
   292  			Etag:           lsObj.ETag,
   293  		}
   294  		// only if single object with or without versions; get legalhold, retention and tags
   295  		if !lsObj.IsDeleteMarker && listOpts.prefix != "" && !strings.HasSuffix(listOpts.prefix, "/") {
   296  			// Add Legal Hold Status if available
   297  			legalHoldStatus, err := listOpts.client.getObjectLegalHold(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID})
   298  			if err != nil {
   299  				errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
   300  				if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
   301  					ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err))
   302  				}
   303  			} else if legalHoldStatus != nil {
   304  				obj.LegalHoldStatus = string(*legalHoldStatus)
   305  			}
   306  			// Add Retention Status if available
   307  			retention, retUntilDate, err := listOpts.client.getObjectRetention(listOpts.ctx, listOpts.bucketName, lsObj.Key, lsObj.VersionID)
   308  			if err != nil {
   309  				errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
   310  				if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
   311  					ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err))
   312  				}
   313  			} else if retention != nil && retUntilDate != nil {
   314  				date := *retUntilDate
   315  				obj.RetentionMode = string(*retention)
   316  				obj.RetentionUntilDate = date.Format(time.RFC3339)
   317  			}
   318  			objTags, err := listOpts.client.getObjectTagging(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID})
   319  			if err != nil {
   320  				ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err))
   321  			} else {
   322  				obj.Tags = objTags.ToMap()
   323  			}
   324  		}
   325  		objects = append(objects, obj)
   326  		totalObjs++
   327  
   328  		if listOpts.limit != nil {
   329  			if totalObjs >= *listOpts.limit {
   330  				break
   331  			}
   332  		}
   333  	}
   334  	return objects, nil
   335  }
   336  
   337  type httpRange struct {
   338  	Start  int64
   339  	Length int64
   340  }
   341  
   342  // Example:
   343  //
   344  //	"Content-Range": "bytes 100-200/1000"
   345  //	"Content-Range": "bytes 100-200/*"
   346  func getRange(start, end, total int64) string {
   347  	// unknown total: -1
   348  	if total == -1 {
   349  		return fmt.Sprintf("bytes %d-%d/*", start, end)
   350  	}
   351  
   352  	return fmt.Sprintf("bytes %d-%d/%d", start, end, total)
   353  }
   354  
   355  // Example:
   356  //
   357  //	"Range": "bytes=100-200"
   358  //	"Range": "bytes=-50"
   359  //	"Range": "bytes=150-"
   360  //	"Range": "bytes=0-0,-1"
   361  func parseRange(s string, size int64) ([]httpRange, error) {
   362  	if s == "" {
   363  		return nil, nil // header not present
   364  	}
   365  	const b = "bytes="
   366  	if !strings.HasPrefix(s, b) {
   367  		return nil, errors.New("invalid range")
   368  	}
   369  	var ranges []httpRange
   370  	for _, ra := range strings.Split(s[len(b):], ",") {
   371  		ra = strings.TrimSpace(ra)
   372  		if ra == "" {
   373  			continue
   374  		}
   375  		i := strings.Index(ra, "-")
   376  		if i < 0 {
   377  			return nil, errors.New("invalid range")
   378  		}
   379  		start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
   380  		var r httpRange
   381  		if start == "" {
   382  			// If no start is specified, end specifies the
   383  			// range start relative to the end of the file.
   384  			i, err := strconv.ParseInt(end, 10, 64)
   385  			if err != nil {
   386  				return nil, errors.New("invalid range")
   387  			}
   388  			if i > size {
   389  				i = size
   390  			}
   391  			r.Start = size - i
   392  			r.Length = size - r.Start
   393  		} else {
   394  			i, err := strconv.ParseInt(start, 10, 64)
   395  			if err != nil || i >= size || i < 0 {
   396  				return nil, errors.New("invalid range")
   397  			}
   398  			r.Start = i
   399  			if end == "" {
   400  				// If no end is specified, range extends to end of the file.
   401  				r.Length = size - r.Start
   402  			} else {
   403  				i, err := strconv.ParseInt(end, 10, 64)
   404  				if err != nil || r.Start > i {
   405  					return nil, errors.New("invalid range")
   406  				}
   407  				if i >= size {
   408  					i = size - 1
   409  				}
   410  				r.Length = i - r.Start + 1
   411  			}
   412  		}
   413  		ranges = append(ranges, r)
   414  	}
   415  	return ranges, nil
   416  }
   417  
   418  func getDownloadObjectResponse(session *models.Principal, params objectApi.DownloadObjectParams) (middleware.Responder, *CodedAPIError) {
   419  	ctx := params.HTTPRequest.Context()
   420  	var prefix string
   421  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   422  	if err != nil {
   423  		return nil, ErrorWithContext(ctx, err)
   424  	}
   425  	if params.Prefix != "" {
   426  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
   427  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
   428  		if err != nil {
   429  			return nil, ErrorWithContext(ctx, err)
   430  		}
   431  		prefix = string(decodedPrefix)
   432  	}
   433  
   434  	opts := minio.GetObjectOptions{}
   435  
   436  	if params.VersionID != nil && *params.VersionID != "" {
   437  		opts.VersionID = *params.VersionID
   438  	}
   439  
   440  	resp, err := mClient.GetObject(ctx, params.BucketName, prefix, opts)
   441  	if err != nil {
   442  		return nil, ErrorWithContext(ctx, err)
   443  	}
   444  
   445  	return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
   446  		defer resp.Close()
   447  
   448  		isPreview := params.Preview != nil && *params.Preview
   449  		// override filename is set
   450  		decodeOverride, err := base64.StdEncoding.DecodeString(*params.OverrideFileName)
   451  		if err != nil {
   452  			fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to decode OverrideFileName: %v", err))
   453  			http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusBadRequest)
   454  			return
   455  		}
   456  
   457  		overrideName := string(decodeOverride)
   458  
   459  		// indicate it's a download / inline content to the browser, and the size of the object
   460  		var filename string
   461  		prefixElements := strings.Split(prefix, "/")
   462  		if len(prefixElements) > 0 && overrideName == "" {
   463  			if prefixElements[len(prefixElements)-1] == "" {
   464  				filename = prefixElements[len(prefixElements)-2]
   465  			} else {
   466  				filename = prefixElements[len(prefixElements)-1]
   467  			}
   468  		} else if overrideName != "" {
   469  			filename = overrideName
   470  		}
   471  
   472  		escapedName := url.PathEscape(filename)
   473  
   474  		// indicate object size & content type
   475  		stat, err := resp.Stat()
   476  		if err != nil {
   477  			minErr := minio.ToErrorResponse(err)
   478  			fmtError := ErrorWithContext(ctx, fmt.Errorf("failed to get Stat() response from server for %s (version %s): %v", prefix, opts.VersionID, minErr.Error()))
   479  			http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError)
   480  			return
   481  		}
   482  
   483  		// if we are getting a Range Request (video) handle that specially
   484  		ranges, err := parseRange(params.HTTPRequest.Header.Get("Range"), stat.Size)
   485  		if err != nil {
   486  			fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to parse range header input %s: %v", params.HTTPRequest.Header.Get("Range"), err))
   487  			http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError)
   488  			return
   489  		}
   490  		contentType := stat.ContentType
   491  		rw.Header().Set("X-XSS-Protection", "1; mode=block")
   492  
   493  		if isPreview && isSafeToPreview(contentType) {
   494  			rw.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", escapedName))
   495  			rw.Header().Set("X-Frame-Options", "SAMEORIGIN")
   496  		} else {
   497  			rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", escapedName))
   498  		}
   499  
   500  		rw.Header().Set("Last-Modified", stat.LastModified.UTC().Format(http.TimeFormat))
   501  
   502  		if isPreview {
   503  			// In case content type was uploaded as octet-stream, we double verify content type
   504  			if stat.ContentType == "application/octet-stream" {
   505  				contentType = mimedb.TypeByExtension(filepath.Ext(escapedName))
   506  			}
   507  		}
   508  		rw.Header().Set("Content-Type", contentType)
   509  		length := stat.Size
   510  		if len(ranges) > 0 {
   511  			start := ranges[0].Start
   512  			length = ranges[0].Length
   513  
   514  			_, err = resp.Seek(start, io.SeekStart)
   515  			if err != nil {
   516  				fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to seek at offset %d: %v", start, err))
   517  				http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError)
   518  				return
   519  			}
   520  
   521  			rw.Header().Set("Accept-Ranges", "bytes")
   522  			rw.Header().Set("Access-Control-Allow-Origin", "*")
   523  			rw.Header().Set("Content-Range", getRange(start, start+length-1, stat.Size))
   524  			rw.WriteHeader(http.StatusPartialContent)
   525  		}
   526  
   527  		rw.Header().Set("Content-Length", fmt.Sprintf("%d", length))
   528  		_, err = io.Copy(rw, io.LimitReader(resp, length))
   529  		if err != nil {
   530  			ErrorWithContext(ctx, fmt.Errorf("unable to write all data to client: %v", err))
   531  			// You can't change headers after you already started writing the body.
   532  			// Handle incomplete write in client.
   533  			return
   534  		}
   535  	}), nil
   536  }
   537  
   538  func getDownloadFolderResponse(session *models.Principal, params objectApi.DownloadObjectParams) (middleware.Responder, *CodedAPIError) {
   539  	ctx := params.HTTPRequest.Context()
   540  	var prefix string
   541  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   542  	if params.Prefix != "" {
   543  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
   544  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
   545  		if err != nil {
   546  			return nil, ErrorWithContext(ctx, err)
   547  		}
   548  		prefix = string(decodedPrefix)
   549  	}
   550  
   551  	folders := strings.Split(prefix, "/")
   552  
   553  	if err != nil {
   554  		return nil, ErrorWithContext(ctx, err)
   555  	}
   556  	minioClient := minioClient{client: mClient}
   557  	objects, err := listBucketObjects(ListObjectsOpts{
   558  		ctx:          ctx,
   559  		client:       minioClient,
   560  		bucketName:   params.BucketName,
   561  		prefix:       prefix,
   562  		recursive:    true,
   563  		withVersions: false,
   564  		withMetadata: false,
   565  	})
   566  	if err != nil {
   567  		return nil, ErrorWithContext(ctx, err)
   568  	}
   569  
   570  	resp, pw := io.Pipe()
   571  	// Create file async
   572  	go func() {
   573  		defer pw.Close()
   574  		zipw := zip.NewWriter(pw)
   575  		var folder string
   576  		if len(folders) > 1 {
   577  			folder = folders[len(folders)-2]
   578  		}
   579  		defer zipw.Close()
   580  
   581  		for i, obj := range objects {
   582  			name := folder + objects[i].Name[len(prefix)-1:]
   583  			object, err := mClient.GetObject(ctx, params.BucketName, obj.Name, minio.GetObjectOptions{})
   584  			if err != nil {
   585  				// Ignore errors, move to next
   586  				continue
   587  			}
   588  			modified, _ := time.Parse(time.RFC3339, obj.LastModified)
   589  			f, err := zipw.CreateHeader(&zip.FileHeader{
   590  				Name:     name,
   591  				NonUTF8:  false,
   592  				Method:   zip.Deflate,
   593  				Modified: modified,
   594  			})
   595  			if err != nil {
   596  				// Ignore errors, move to next
   597  				continue
   598  			}
   599  			_, err = io.Copy(f, object)
   600  			if err != nil {
   601  				// We have a partial object, report error.
   602  				pw.CloseWithError(err)
   603  				return
   604  			}
   605  		}
   606  	}()
   607  
   608  	return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
   609  		defer resp.Close()
   610  
   611  		// indicate it's a download / inline content to the browser, and the size of the object
   612  		var prefixPath string
   613  		var filename string
   614  		if params.Prefix != "" {
   615  			encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
   616  			decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
   617  			if err != nil {
   618  				fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to parse encoded prefix %s: %v", encodedPrefix, err))
   619  				http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError)
   620  				return
   621  			}
   622  
   623  			prefixPath = string(decodedPrefix)
   624  		}
   625  		prefixElements := strings.Split(prefixPath, "/")
   626  		if len(prefixElements) > 0 {
   627  			if prefixElements[len(prefixElements)-1] == "" {
   628  				filename = prefixElements[len(prefixElements)-2]
   629  			} else {
   630  				filename = prefixElements[len(prefixElements)-1]
   631  			}
   632  		}
   633  		escapedName := url.PathEscape(filename)
   634  
   635  		rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", escapedName))
   636  		rw.Header().Set("Content-Type", "application/zip")
   637  
   638  		// Copy the stream
   639  		_, err := io.Copy(rw, resp)
   640  		if err != nil {
   641  			ErrorWithContext(ctx, fmt.Errorf("unable to write all the requested data: %v", err))
   642  			// You can't change headers after you already started writing the body.
   643  			// Handle incomplete write in client.
   644  			return
   645  		}
   646  	}), nil
   647  }
   648  
   649  func getMultipleFilesDownloadResponse(session *models.Principal, params objectApi.DownloadMultipleObjectsParams) (middleware.Responder, *CodedAPIError) {
   650  	ctx := params.HTTPRequest.Context()
   651  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   652  	if err != nil {
   653  		return nil, ErrorWithContext(ctx, err)
   654  	}
   655  	minioClient := minioClient{client: mClient}
   656  
   657  	resp, pw := io.Pipe()
   658  	// Create file async
   659  	go func() {
   660  		defer pw.Close()
   661  		zipw := zip.NewWriter(pw)
   662  		defer zipw.Close()
   663  
   664  		addToZip := func(name string, modified time.Time) (io.Writer, error) {
   665  			f, err := zipw.CreateHeader(&zip.FileHeader{
   666  				Name:     name,
   667  				NonUTF8:  false,
   668  				Method:   zip.Deflate,
   669  				Modified: modified,
   670  			})
   671  			return f, err
   672  		}
   673  
   674  		for _, dObj := range params.ObjectList {
   675  			// if a prefix is selected, list and add objects recursively
   676  			// the prefixes are not base64 encoded.
   677  			if strings.HasSuffix(dObj, "/") {
   678  				prefix := dObj
   679  
   680  				folders := strings.Split(prefix, "/")
   681  
   682  				var folder string
   683  				if len(folders) > 1 {
   684  					folder = folders[len(folders)-2]
   685  				}
   686  
   687  				objects, err := listBucketObjects(ListObjectsOpts{
   688  					ctx:          ctx,
   689  					client:       minioClient,
   690  					bucketName:   params.BucketName,
   691  					prefix:       prefix,
   692  					recursive:    true,
   693  					withVersions: false,
   694  					withMetadata: false,
   695  				})
   696  				if err != nil {
   697  					pw.CloseWithError(err)
   698  				}
   699  
   700  				for i, obj := range objects {
   701  					name := folder + objects[i].Name[len(prefix)-1:]
   702  
   703  					object, err := mClient.GetObject(ctx, params.BucketName, obj.Name, minio.GetObjectOptions{})
   704  					if err != nil {
   705  						// Ignore errors, move to next
   706  						continue
   707  					}
   708  					modified, _ := time.Parse(time.RFC3339, obj.LastModified)
   709  
   710  					f, err := addToZip(name, modified)
   711  					if err != nil {
   712  						// Ignore errors, move to next
   713  						continue
   714  					}
   715  					_, err = io.Copy(f, object)
   716  					if err != nil {
   717  						// We have a partial object, report error.
   718  						pw.CloseWithError(err)
   719  						return
   720  					}
   721  				}
   722  
   723  			} else {
   724  				// add selected individual object
   725  				objectData, err := mClient.StatObject(ctx, params.BucketName, dObj, minio.StatObjectOptions{})
   726  				if err != nil {
   727  					// Ignore errors, move to next
   728  					continue
   729  				}
   730  				object, err := mClient.GetObject(ctx, params.BucketName, dObj, minio.GetObjectOptions{})
   731  				if err != nil {
   732  					// Ignore errors, move to next
   733  					continue
   734  				}
   735  
   736  				prefixes := strings.Split(dObj, "/")
   737  				// truncate upper level prefixes to make the download as flat at the current level.
   738  				objectName := prefixes[len(prefixes)-1]
   739  				f, err := addToZip(objectName, objectData.LastModified)
   740  				if err != nil {
   741  					// Ignore errors, move to next
   742  					continue
   743  				}
   744  				_, err = io.Copy(f, object)
   745  				if err != nil {
   746  					// We have a partial object, report error.
   747  					pw.CloseWithError(err)
   748  					return
   749  				}
   750  			}
   751  		}
   752  	}()
   753  
   754  	return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
   755  		defer resp.Close()
   756  
   757  		// indicate it's a download / inline content to the browser, and the size of the object
   758  		fileName := "selected_files_" + strings.ReplaceAll(strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339), ":", ""), "-", "")
   759  
   760  		rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", fileName))
   761  		rw.Header().Set("Content-Type", "application/zip")
   762  
   763  		// Copy the stream
   764  		_, err := io.Copy(rw, resp)
   765  		if err != nil {
   766  			ErrorWithContext(ctx, fmt.Errorf("unable to write all the requested data: %v", err))
   767  			// You can't change headers after you already started writing the body.
   768  			// Handle incomplete write in client.
   769  			return
   770  		}
   771  	}), nil
   772  }
   773  
   774  // getDeleteObjectResponse returns whether there was an error on deletion of object
   775  func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteObjectParams) *CodedAPIError {
   776  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   777  	defer cancel()
   778  	var prefix string
   779  	if params.Prefix != "" {
   780  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
   781  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
   782  		if err != nil {
   783  			return ErrorWithContext(ctx, err)
   784  		}
   785  		prefix = string(decodedPrefix)
   786  	}
   787  	s3Client, err := newS3BucketClient(session, params.BucketName, prefix, getClientIP(params.HTTPRequest))
   788  	if err != nil {
   789  		return ErrorWithContext(ctx, err)
   790  	}
   791  	// create a mc S3Client interface implementation
   792  	// defining the client to be used
   793  	mcClient := mcClient{client: s3Client}
   794  	var rec bool
   795  	var version string
   796  	var allVersions bool
   797  	var nonCurrentVersions bool
   798  	var bypass bool
   799  	if params.Recursive != nil {
   800  		rec = *params.Recursive
   801  	}
   802  	if params.VersionID != nil {
   803  		version = *params.VersionID
   804  	}
   805  	if params.AllVersions != nil {
   806  		allVersions = *params.AllVersions
   807  	}
   808  	if params.NonCurrentVersions != nil {
   809  		nonCurrentVersions = *params.NonCurrentVersions
   810  	}
   811  	if params.Bypass != nil {
   812  		bypass = *params.Bypass
   813  	}
   814  
   815  	if allVersions && nonCurrentVersions {
   816  		err := errors.New("cannot set delete all versions and delete non-current versions flags at the same time")
   817  		return ErrorWithContext(ctx, err)
   818  	}
   819  
   820  	err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions, nonCurrentVersions, bypass)
   821  	if err != nil {
   822  		return ErrorWithContext(ctx, err)
   823  	}
   824  	return nil
   825  }
   826  
   827  // getDeleteMultiplePathsResponse returns whether there was an error on deletion of any object
   828  func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi.DeleteMultipleObjectsParams) *CodedAPIError {
   829  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   830  	defer cancel()
   831  	var version string
   832  	var allVersions bool
   833  	var bypass bool
   834  	if params.AllVersions != nil {
   835  		allVersions = *params.AllVersions
   836  	}
   837  	if params.Bypass != nil {
   838  		bypass = *params.Bypass
   839  	}
   840  	for i := 0; i < len(params.Files); i++ {
   841  		if params.Files[i].VersionID != "" {
   842  			version = params.Files[i].VersionID
   843  		}
   844  		prefix := params.Files[i].Path
   845  		s3Client, err := newS3BucketClient(session, params.BucketName, prefix, getClientIP(params.HTTPRequest))
   846  		if err != nil {
   847  			return ErrorWithContext(ctx, err)
   848  		}
   849  		// create a mc S3Client interface implementation
   850  		// defining the client to be used
   851  		mcClient := mcClient{client: s3Client}
   852  		err = deleteObjects(ctx, mcClient, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions, false, bypass)
   853  		if err != nil {
   854  			return ErrorWithContext(ctx, err)
   855  		}
   856  	}
   857  	return nil
   858  }
   859  
   860  // deleteObjects deletes either a single object or multiple objects based on recursive flag
   861  func deleteObjects(ctx context.Context, client MCClient, bucket string, path string, versionID string, recursive, allVersions, nonCurrentVersionsOnly, bypass bool) error {
   862  	// Delete All non-Current versions only.
   863  	if nonCurrentVersionsOnly {
   864  		return deleteNonCurrentVersions(ctx, client, bypass)
   865  	}
   866  
   867  	if recursive || allVersions {
   868  		return deleteMultipleObjects(ctx, client, path, recursive, allVersions, bypass)
   869  	}
   870  
   871  	return deleteSingleObject(ctx, client, bucket, path, versionID, bypass)
   872  }
   873  
   874  // Return standardized URL to be used to compare later.
   875  func getStandardizedURL(targetURL string) string {
   876  	return filepath.FromSlash(targetURL)
   877  }
   878  
   879  // deleteMultipleObjects uses listing before removal, it can list recursively or not,
   880  //
   881  //	Use cases:
   882  //	   * Remove objects recursively
   883  func deleteMultipleObjects(ctx context.Context, client MCClient, path string, recursive, allVersions, isBypass bool) error {
   884  	// Constants defined to make this code more readable
   885  	const (
   886  		isIncomplete   = false
   887  		isRemoveBucket = false
   888  		forceDelete    = false // Force delete not meant to be used by console UI.
   889  	)
   890  
   891  	listOpts := mc.ListOptions{
   892  		Recursive:         recursive,
   893  		Incomplete:        isIncomplete,
   894  		ShowDir:           mc.DirNone,
   895  		WithOlderVersions: allVersions,
   896  		WithDeleteMarkers: allVersions,
   897  	}
   898  
   899  	lctx, cancel := context.WithCancel(ctx)
   900  	defer cancel()
   901  
   902  	contentCh := make(chan *mc.ClientContent)
   903  
   904  	go func() {
   905  		defer close(contentCh)
   906  
   907  		for content := range client.list(lctx, listOpts) {
   908  			if content.Err != nil {
   909  				continue
   910  			}
   911  
   912  			if !strings.HasSuffix(getStandardizedURL(content.URL.Path), path) && !strings.HasSuffix(path, "/") {
   913  				continue
   914  			}
   915  
   916  			select {
   917  			case contentCh <- content:
   918  			case <-lctx.Done():
   919  				return
   920  			}
   921  		}
   922  	}()
   923  
   924  	for result := range client.remove(ctx, isIncomplete, isRemoveBucket, isBypass, forceDelete, contentCh) {
   925  		if result.Err != nil {
   926  			return result.Err.Cause
   927  		}
   928  	}
   929  
   930  	return nil
   931  }
   932  
   933  func deleteSingleObject(ctx context.Context, client MCClient, bucket, object string, versionID string, isBypass bool) error {
   934  	targetURL := fmt.Sprintf("%s/%s", bucket, object)
   935  	contentCh := make(chan *mc.ClientContent, 1)
   936  	contentCh <- &mc.ClientContent{URL: *newClientURL(targetURL), VersionID: versionID}
   937  	close(contentCh)
   938  
   939  	isIncomplete := false
   940  	isRemoveBucket := false
   941  
   942  	resultCh := client.remove(ctx, isIncomplete, isRemoveBucket, isBypass, false, contentCh)
   943  	for result := range resultCh {
   944  		if result.Err != nil {
   945  			return result.Err.Cause
   946  		}
   947  	}
   948  	return nil
   949  }
   950  
   951  func deleteNonCurrentVersions(ctx context.Context, client MCClient, isBypass bool) error {
   952  	lctx, cancel := context.WithCancel(ctx)
   953  	defer cancel()
   954  
   955  	contentCh := make(chan *mc.ClientContent)
   956  
   957  	go func() {
   958  		defer close(contentCh)
   959  
   960  		// Get current object versions
   961  		for lsObj := range client.list(lctx, mc.ListOptions{
   962  			WithDeleteMarkers: true,
   963  			WithOlderVersions: true,
   964  			Recursive:         true,
   965  		}) {
   966  			if lsObj.Err != nil {
   967  				continue
   968  			}
   969  
   970  			if lsObj.IsLatest {
   971  				continue
   972  			}
   973  
   974  			// All non-current objects proceed to purge.
   975  			select {
   976  			case contentCh <- lsObj:
   977  			case <-lctx.Done():
   978  				return
   979  			}
   980  		}
   981  	}()
   982  
   983  	for result := range client.remove(ctx, false, false, isBypass, false, contentCh) {
   984  		if result.Err != nil {
   985  			return result.Err.Cause
   986  		}
   987  	}
   988  
   989  	return nil
   990  }
   991  
   992  func getUploadObjectResponse(session *models.Principal, params objectApi.PostBucketsBucketNameObjectsUploadParams) *CodedAPIError {
   993  	ctx := params.HTTPRequest.Context()
   994  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   995  	if err != nil {
   996  		return ErrorWithContext(ctx, err)
   997  	}
   998  	// create a minioClient interface implementation
   999  	// defining the client to be used
  1000  	minioClient := minioClient{client: mClient}
  1001  	if err := uploadFiles(ctx, minioClient, params); err != nil {
  1002  		return ErrorWithContext(ctx, err, ErrDefault)
  1003  	}
  1004  	return nil
  1005  }
  1006  
  1007  // uploadFiles gets files from http.Request form and uploads them to MinIO
  1008  func uploadFiles(ctx context.Context, client MinioClient, params objectApi.PostBucketsBucketNameObjectsUploadParams) error {
  1009  	var prefix string
  1010  	if params.Prefix != nil {
  1011  		encodedPrefix := SanitizeEncodedPrefix(*params.Prefix)
  1012  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1013  		if err != nil {
  1014  			return err
  1015  		}
  1016  		prefix = string(decodedPrefix)
  1017  		// trim any leading '/', since that is not expected
  1018  		// for any object.
  1019  		prefix = strings.TrimPrefix(prefix, "/")
  1020  	}
  1021  
  1022  	// parse a request body as multipart/form-data.
  1023  	// 32 << 20 is default max memory
  1024  	mr, err := params.HTTPRequest.MultipartReader()
  1025  	if err != nil {
  1026  		return err
  1027  	}
  1028  
  1029  	for {
  1030  		p, err := mr.NextPart()
  1031  		if err == io.EOF {
  1032  			break
  1033  		}
  1034  
  1035  		size, err := strconv.ParseInt(p.FormName(), 10, 64)
  1036  		if err != nil {
  1037  			return err
  1038  		}
  1039  
  1040  		contentType := p.Header.Get("content-type")
  1041  		if contentType == "" {
  1042  			contentType = mimedb.TypeByExtension(filepath.Ext(p.FileName()))
  1043  		}
  1044  		objectName := prefix // prefix will have complete object path e.g: /test-prefix/test-object.txt
  1045  		_, err = client.putObject(ctx, params.BucketName, objectName, p, size, minio.PutObjectOptions{
  1046  			ContentType:      contentType,
  1047  			DisableMultipart: true, // Do not upload as multipart stream for console uploader.
  1048  		})
  1049  		if err != nil {
  1050  			return err
  1051  		}
  1052  	}
  1053  
  1054  	return nil
  1055  }
  1056  
  1057  // getShareObjectResponse returns a share object url
  1058  func getShareObjectResponse(session *models.Principal, params objectApi.ShareObjectParams) (*string, *CodedAPIError) {
  1059  	ctx := params.HTTPRequest.Context()
  1060  	clientIP := utils.ClientIPFromContext(ctx)
  1061  	var prefix string
  1062  	if params.Prefix != "" {
  1063  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1064  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1065  		if err != nil {
  1066  			return nil, ErrorWithContext(ctx, err)
  1067  		}
  1068  		prefix = string(decodedPrefix)
  1069  	}
  1070  	s3Client, err := newS3BucketClient(session, params.BucketName, prefix, clientIP)
  1071  	if err != nil {
  1072  		return nil, ErrorWithContext(ctx, err)
  1073  	}
  1074  	// create a mc S3Client interface implementation
  1075  	// defining the client to be used
  1076  	mcClient := mcClient{client: s3Client}
  1077  	var expireDuration string
  1078  	if params.Expires != nil {
  1079  		expireDuration = *params.Expires
  1080  	}
  1081  	url, err := getShareObjectURL(ctx, mcClient, params.HTTPRequest, params.VersionID, expireDuration)
  1082  	if err != nil {
  1083  		return nil, ErrorWithContext(ctx, err)
  1084  	}
  1085  
  1086  	return url, nil
  1087  }
  1088  
  1089  func getShareObjectURL(ctx context.Context, client MCClient, r *http.Request, versionID string, duration string) (url *string, err error) {
  1090  	// default duration 7d if not defined
  1091  	if strings.TrimSpace(duration) == "" {
  1092  		duration = "168h"
  1093  	}
  1094  	expiresDuration, err := time.ParseDuration(duration)
  1095  	if err != nil {
  1096  		return nil, err
  1097  	}
  1098  	minioURL, pErr := client.shareDownload(ctx, versionID, expiresDuration)
  1099  	if pErr != nil {
  1100  		return nil, pErr.Cause
  1101  	}
  1102  
  1103  	encodedMinIOURL := b64.URLEncoding.EncodeToString([]byte(minioURL))
  1104  	requestURL := getRequestURLWithScheme(r)
  1105  	objURL := fmt.Sprintf("%s/api/v1/download-shared-object/%s", requestURL, encodedMinIOURL)
  1106  	return &objURL, nil
  1107  }
  1108  
  1109  func getRequestURLWithScheme(r *http.Request) string {
  1110  	scheme := "http"
  1111  	if r.TLS != nil {
  1112  		scheme = "https"
  1113  	}
  1114  
  1115  	redirectURL := getConsoleBrowserRedirectURL()
  1116  	if redirectURL != "" {
  1117  		return strings.TrimSuffix(redirectURL, "/")
  1118  	}
  1119  
  1120  	return fmt.Sprintf("%s://%s", scheme, r.Host)
  1121  }
  1122  
  1123  func getSetObjectLegalHoldResponse(session *models.Principal, params objectApi.PutObjectLegalHoldParams) *CodedAPIError {
  1124  	ctx := params.HTTPRequest.Context()
  1125  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
  1126  	if err != nil {
  1127  		return ErrorWithContext(ctx, err)
  1128  	}
  1129  	// create a minioClient interface implementation
  1130  	// defining the client to be used
  1131  	minioClient := minioClient{client: mClient}
  1132  	var prefix string
  1133  	if params.Prefix != "" {
  1134  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1135  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1136  		if err != nil {
  1137  			return ErrorWithContext(ctx, err)
  1138  		}
  1139  		prefix = string(decodedPrefix)
  1140  	}
  1141  	err = setObjectLegalHold(ctx, minioClient, params.BucketName, prefix, params.VersionID, *params.Body.Status)
  1142  	if err != nil {
  1143  		return ErrorWithContext(ctx, err)
  1144  	}
  1145  	return nil
  1146  }
  1147  
  1148  func setObjectLegalHold(ctx context.Context, client MinioClient, bucketName, prefix, versionID string, status models.ObjectLegalHoldStatus) error {
  1149  	var lstatus minio.LegalHoldStatus
  1150  	if status == models.ObjectLegalHoldStatusEnabled {
  1151  		lstatus = minio.LegalHoldEnabled
  1152  	} else {
  1153  		lstatus = minio.LegalHoldDisabled
  1154  	}
  1155  	return client.putObjectLegalHold(ctx, bucketName, prefix, minio.PutObjectLegalHoldOptions{VersionID: versionID, Status: &lstatus})
  1156  }
  1157  
  1158  func getSetObjectRetentionResponse(session *models.Principal, params objectApi.PutObjectRetentionParams) *CodedAPIError {
  1159  	ctx := params.HTTPRequest.Context()
  1160  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
  1161  	if err != nil {
  1162  		return ErrorWithContext(ctx, err)
  1163  	}
  1164  	// create a minioClient interface implementation
  1165  	// defining the client to be used
  1166  	minioClient := minioClient{client: mClient}
  1167  	var prefix string
  1168  	if params.Prefix != "" {
  1169  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1170  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1171  		if err != nil {
  1172  			return ErrorWithContext(ctx, err)
  1173  		}
  1174  		prefix = string(decodedPrefix)
  1175  	}
  1176  	err = setObjectRetention(ctx, minioClient, params.BucketName, params.VersionID, prefix, params.Body)
  1177  	if err != nil {
  1178  		return ErrorWithContext(ctx, err)
  1179  	}
  1180  	return nil
  1181  }
  1182  
  1183  func setObjectRetention(ctx context.Context, client MinioClient, bucketName, versionID, prefix string, retentionOps *models.PutObjectRetentionRequest) error {
  1184  	if retentionOps == nil {
  1185  		return errors.New("object retention options can't be nil")
  1186  	}
  1187  	if retentionOps.Expires == nil {
  1188  		return errors.New("object retention expires can't be nil")
  1189  	}
  1190  
  1191  	var mode minio.RetentionMode
  1192  	if *retentionOps.Mode == models.ObjectRetentionModeGovernance {
  1193  		mode = minio.Governance
  1194  	} else {
  1195  		mode = minio.Compliance
  1196  	}
  1197  	retentionUntilDate, err := time.Parse(time.RFC3339, *retentionOps.Expires)
  1198  	if err != nil {
  1199  		return err
  1200  	}
  1201  	opts := minio.PutObjectRetentionOptions{
  1202  		GovernanceBypass: retentionOps.GovernanceBypass,
  1203  		RetainUntilDate:  &retentionUntilDate,
  1204  		Mode:             &mode,
  1205  		VersionID:        versionID,
  1206  	}
  1207  	return client.putObjectRetention(ctx, bucketName, prefix, opts)
  1208  }
  1209  
  1210  func deleteObjectRetentionResponse(session *models.Principal, params objectApi.DeleteObjectRetentionParams) *CodedAPIError {
  1211  	ctx := params.HTTPRequest.Context()
  1212  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
  1213  	if err != nil {
  1214  		return ErrorWithContext(ctx, err)
  1215  	}
  1216  	// create a minioClient interface implementation
  1217  	// defining the client to be used
  1218  	minioClient := minioClient{client: mClient}
  1219  	var prefix string
  1220  	if params.Prefix != "" {
  1221  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1222  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1223  		if err != nil {
  1224  			return ErrorWithContext(ctx, err)
  1225  		}
  1226  		prefix = string(decodedPrefix)
  1227  	}
  1228  	err = deleteObjectRetention(ctx, minioClient, params.BucketName, prefix, params.VersionID)
  1229  	if err != nil {
  1230  		return ErrorWithContext(ctx, err)
  1231  	}
  1232  	return nil
  1233  }
  1234  
  1235  func deleteObjectRetention(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) error {
  1236  	opts := minio.PutObjectRetentionOptions{
  1237  		GovernanceBypass: true,
  1238  		VersionID:        versionID,
  1239  	}
  1240  
  1241  	return client.putObjectRetention(ctx, bucketName, prefix, opts)
  1242  }
  1243  
  1244  func getPutObjectTagsResponse(session *models.Principal, params objectApi.PutObjectTagsParams) *CodedAPIError {
  1245  	ctx := params.HTTPRequest.Context()
  1246  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
  1247  	if err != nil {
  1248  		return ErrorWithContext(ctx, err)
  1249  	}
  1250  	// create a minioClient interface implementation
  1251  	// defining the client to be used
  1252  	minioClient := minioClient{client: mClient}
  1253  	var prefix string
  1254  	if params.Prefix != "" {
  1255  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1256  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1257  		if err != nil {
  1258  			return ErrorWithContext(ctx, err)
  1259  		}
  1260  		prefix = string(decodedPrefix)
  1261  	}
  1262  	err = putObjectTags(ctx, minioClient, params.BucketName, prefix, params.VersionID, params.Body.Tags)
  1263  	if err != nil {
  1264  		return ErrorWithContext(ctx, err)
  1265  	}
  1266  	return nil
  1267  }
  1268  
  1269  func putObjectTags(ctx context.Context, client MinioClient, bucketName, prefix, versionID string, tagMap map[string]string) error {
  1270  	opt := minio.PutObjectTaggingOptions{
  1271  		VersionID: versionID,
  1272  	}
  1273  	otags, err := tags.MapToObjectTags(tagMap)
  1274  	if err != nil {
  1275  		return err
  1276  	}
  1277  	return client.putObjectTagging(ctx, bucketName, prefix, otags, opt)
  1278  }
  1279  
  1280  // Restore Object Version
  1281  func getPutObjectRestoreResponse(session *models.Principal, params objectApi.PutObjectRestoreParams) *CodedAPIError {
  1282  	ctx := params.HTTPRequest.Context()
  1283  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
  1284  	if err != nil {
  1285  		return ErrorWithContext(ctx, err)
  1286  	}
  1287  	// create a minioClient interface implementation
  1288  	// defining the client to be used
  1289  	minioClient := minioClient{client: mClient}
  1290  
  1291  	var prefix string
  1292  	if params.Prefix != "" {
  1293  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1294  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1295  		if err != nil {
  1296  			return ErrorWithContext(ctx, err)
  1297  		}
  1298  		prefix = string(decodedPrefix)
  1299  	}
  1300  
  1301  	err = restoreObject(ctx, minioClient, params.BucketName, prefix, params.VersionID)
  1302  	if err != nil {
  1303  		return ErrorWithContext(ctx, err)
  1304  	}
  1305  	return nil
  1306  }
  1307  
  1308  func restoreObject(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) error {
  1309  	// Select required version
  1310  	srcOpts := minio.CopySrcOptions{
  1311  		Bucket:    bucketName,
  1312  		Object:    prefix,
  1313  		VersionID: versionID,
  1314  	}
  1315  
  1316  	// Destination object, same as current bucket
  1317  	replaceMetadata := make(map[string]string)
  1318  	replaceMetadata["copy-source"] = versionID
  1319  
  1320  	dstOpts := minio.CopyDestOptions{
  1321  		Bucket:       bucketName,
  1322  		Object:       prefix,
  1323  		UserMetadata: replaceMetadata,
  1324  	}
  1325  
  1326  	// Copy object call
  1327  	_, err := client.copyObject(ctx, dstOpts, srcOpts)
  1328  	if err != nil {
  1329  		return err
  1330  	}
  1331  
  1332  	return nil
  1333  }
  1334  
  1335  // Metadata Response from minio-go API
  1336  func getObjectMetadataResponse(session *models.Principal, params objectApi.GetObjectMetadataParams) (*models.Metadata, *CodedAPIError) {
  1337  	ctx := params.HTTPRequest.Context()
  1338  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
  1339  	if err != nil {
  1340  		return nil, ErrorWithContext(ctx, err)
  1341  	}
  1342  	// create a minioClient interface implementation
  1343  	// defining the client to be used
  1344  	minioClient := minioClient{client: mClient}
  1345  	var prefix string
  1346  	var versionID string
  1347  
  1348  	if params.Prefix != "" {
  1349  		encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
  1350  		decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
  1351  		if err != nil {
  1352  			return nil, ErrorWithContext(ctx, err)
  1353  		}
  1354  		prefix = string(decodedPrefix)
  1355  	}
  1356  
  1357  	if params.VersionID != nil {
  1358  		versionID = *params.VersionID
  1359  	}
  1360  
  1361  	objectInfo, err := getObjectInfo(ctx, minioClient, params.BucketName, prefix, versionID)
  1362  	if err != nil {
  1363  		return nil, ErrorWithContext(ctx, err)
  1364  	}
  1365  
  1366  	metadata := &models.Metadata{ObjectMetadata: objectInfo.Metadata}
  1367  
  1368  	return metadata, nil
  1369  }
  1370  
  1371  func getObjectInfo(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) (minio.ObjectInfo, error) {
  1372  	objectData, err := client.statObject(ctx, bucketName, prefix, minio.GetObjectOptions{VersionID: versionID})
  1373  	if err != nil {
  1374  		return minio.ObjectInfo{}, err
  1375  	}
  1376  
  1377  	return objectData, nil
  1378  }
  1379  
  1380  // newClientURL returns an abstracted URL for filesystems and object storage.
  1381  func newClientURL(urlStr string) *mc.ClientURL {
  1382  	scheme, rest := getScheme(urlStr)
  1383  	if strings.HasPrefix(rest, "//") {
  1384  		// if rest has '//' prefix, skip them
  1385  		var authority string
  1386  		authority, rest = splitSpecial(rest[2:], "/", false)
  1387  		if rest == "" {
  1388  			rest = "/"
  1389  		}
  1390  		host := getHost(authority)
  1391  		if host != "" && (scheme == "http" || scheme == "https") {
  1392  			return &mc.ClientURL{
  1393  				Scheme:          scheme,
  1394  				Type:            objectStorage,
  1395  				Host:            host,
  1396  				Path:            rest,
  1397  				SchemeSeparator: "://",
  1398  				Separator:       '/',
  1399  			}
  1400  		}
  1401  	}
  1402  	return &mc.ClientURL{
  1403  		Type:      fileSystem,
  1404  		Path:      rest,
  1405  		Separator: filepath.Separator,
  1406  	}
  1407  }
  1408  
  1409  // Maybe rawurl is of the form scheme:path. (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*)
  1410  // If so, return scheme, path; else return "", rawurl.
  1411  func getScheme(rawurl string) (scheme, path string) {
  1412  	urlSplits := strings.Split(rawurl, "://")
  1413  	if len(urlSplits) == 2 {
  1414  		scheme, uri := urlSplits[0], "//"+urlSplits[1]
  1415  		// ignore numbers in scheme
  1416  		validScheme := regexp.MustCompile("^[a-zA-Z]+$")
  1417  		if uri != "" {
  1418  			if validScheme.MatchString(scheme) {
  1419  				return scheme, uri
  1420  			}
  1421  		}
  1422  	}
  1423  	return "", rawurl
  1424  }
  1425  
  1426  // Assuming s is of the form [s delimiter s].
  1427  // If so, return s, [delimiter]s or return s, s if cutdelimiter == true
  1428  // If no delimiter found return s, "".
  1429  func splitSpecial(s string, delimiter string, cutdelimiter bool) (string, string) {
  1430  	i := strings.Index(s, delimiter)
  1431  	if i < 0 {
  1432  		// if delimiter not found return as is.
  1433  		return s, ""
  1434  	}
  1435  	// if delimiter should be removed, remove it.
  1436  	if cutdelimiter {
  1437  		return s[0:i], s[i+len(delimiter):]
  1438  	}
  1439  	// return split strings with delimiter
  1440  	return s[0:i], s[i:]
  1441  }
  1442  
  1443  // getHost - extract host from authority string, we do not support ftp style username@ yet.
  1444  func getHost(authority string) (host string) {
  1445  	i := strings.LastIndex(authority, "@")
  1446  	if i >= 0 {
  1447  		// TODO support, username@password style userinfo, useful for ftp support.
  1448  		return
  1449  	}
  1450  	return authority
  1451  }