mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 06:38:37 -04:00 
			
		
		
		
	Add a storage layer for attachments (#11387)
* Add a storage layer for attachments * Fix some bug * fix test * Fix copyright head and lint * Fix bug * Add setting for minio and flags for migrate-storage * Add documents * fix lint * Add test for minio store type on attachments * fix test * fix test * Apply suggestions from code review Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com> * Add warning when storage migrated successfully * Fix drone * fix test * rebase * Fix test * display the error on console * Move minio test to amd64 since minio docker don't support arm64 * refactor the codes * add trace * Fix test * remove log on xorm * Fi download bug * Add a storage layer for attachments * Add setting for minio and flags for migrate-storage * fix lint * Add test for minio store type on attachments * Apply suggestions from code review Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com> * Fix drone * fix test * Fix test * display the error on console * Move minio test to amd64 since minio docker don't support arm64 * refactor the codes * add trace * Fix test * Add URL function to serve attachments directly from S3/Minio * Add ability to enable/disable redirection in attachment configuration * Fix typo * Add a storage layer for attachments * Add setting for minio and flags for migrate-storage * fix lint * Add test for minio store type on attachments * Apply suggestions from code review Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com> * Fix drone * fix test * Fix test * display the error on console * Move minio test to amd64 since minio docker don't support arm64 * don't change unrelated files * Fix lint * Fix build * update go.mod and go.sum * Use github.com/minio/minio-go/v6 * Remove unused function * Upgrade minio to v7 and some other improvements * fix lint * Fix go mod Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com> Co-authored-by: Tyler <tystuyfzand@gmail.com>
This commit is contained in:
		
							
								
								
									
										950
									
								
								vendor/github.com/minio/minio-go/v7/api-list.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										950
									
								
								vendor/github.com/minio/minio-go/v7/api-list.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,950 @@ | ||||
| /* | ||||
|  * MinIO Go Library for Amazon S3 Compatible Cloud Storage | ||||
|  * Copyright 2015-2020 MinIO, Inc. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package minio | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/minio/minio-go/v7/pkg/s3utils" | ||||
| ) | ||||
|  | ||||
| // ListBuckets list all buckets owned by this authenticated user. | ||||
| // | ||||
| // This call requires explicit authentication, no anonymous requests are | ||||
| // allowed for listing buckets. | ||||
| // | ||||
| //   api := client.New(....) | ||||
| //   for message := range api.ListBuckets(context.Background()) { | ||||
| //       fmt.Println(message) | ||||
| //   } | ||||
| // | ||||
| func (c Client) ListBuckets(ctx context.Context) ([]BucketInfo, error) { | ||||
| 	// Execute GET on service. | ||||
| 	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{contentSHA256Hex: emptySHA256Hex}) | ||||
| 	defer closeResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if resp != nil { | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return nil, httpRespToErrorResponse(resp, "", "") | ||||
| 		} | ||||
| 	} | ||||
| 	listAllMyBucketsResult := listAllMyBucketsResult{} | ||||
| 	err = xmlDecoder(resp.Body, &listAllMyBucketsResult) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return listAllMyBucketsResult.Buckets.Bucket, nil | ||||
| } | ||||
|  | ||||
| /// Bucket Read Operations. | ||||
|  | ||||
| func (c Client) listObjectsV2(ctx context.Context, bucketName, objectPrefix string, recursive, metadata bool, maxKeys int) <-chan ObjectInfo { | ||||
| 	// Allocate new list objects channel. | ||||
| 	objectStatCh := make(chan ObjectInfo, 1) | ||||
| 	// Default listing is delimited at "/" | ||||
| 	delimiter := "/" | ||||
| 	if recursive { | ||||
| 		// If recursive we do not delimit. | ||||
| 		delimiter = "" | ||||
| 	} | ||||
|  | ||||
| 	// Return object owner information by default | ||||
| 	fetchOwner := true | ||||
|  | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		defer close(objectStatCh) | ||||
| 		objectStatCh <- ObjectInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return objectStatCh | ||||
| 	} | ||||
|  | ||||
| 	// Validate incoming object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(objectPrefix); err != nil { | ||||
| 		defer close(objectStatCh) | ||||
| 		objectStatCh <- ObjectInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return objectStatCh | ||||
| 	} | ||||
|  | ||||
| 	// Initiate list objects goroutine here. | ||||
| 	go func(objectStatCh chan<- ObjectInfo) { | ||||
| 		defer close(objectStatCh) | ||||
| 		// Save continuationToken for next request. | ||||
| 		var continuationToken string | ||||
| 		for { | ||||
| 			// Get list of objects a maximum of 1000 per request. | ||||
| 			result, err := c.listObjectsV2Query(ctx, bucketName, objectPrefix, continuationToken, | ||||
| 				fetchOwner, metadata, delimiter, maxKeys) | ||||
| 			if err != nil { | ||||
| 				objectStatCh <- ObjectInfo{ | ||||
| 					Err: err, | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// If contents are available loop through and send over channel. | ||||
| 			for _, object := range result.Contents { | ||||
| 				object.ETag = trimEtag(object.ETag) | ||||
| 				select { | ||||
| 				// Send object content. | ||||
| 				case objectStatCh <- object: | ||||
| 				// If receives done from the caller, return here. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Send all common prefixes if any. | ||||
| 			// NOTE: prefixes are only present if the request is delimited. | ||||
| 			for _, obj := range result.CommonPrefixes { | ||||
| 				select { | ||||
| 				// Send object prefixes. | ||||
| 				case objectStatCh <- ObjectInfo{Key: obj.Prefix}: | ||||
| 				// If receives done from the caller, return here. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// If continuation token present, save it for next request. | ||||
| 			if result.NextContinuationToken != "" { | ||||
| 				continuationToken = result.NextContinuationToken | ||||
| 			} | ||||
|  | ||||
| 			// Listing ends result is not truncated, return right here. | ||||
| 			if !result.IsTruncated { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}(objectStatCh) | ||||
| 	return objectStatCh | ||||
| } | ||||
|  | ||||
| // listObjectsV2Query - (List Objects V2) - List some or all (up to 1000) of the objects in a bucket. | ||||
| // | ||||
| // You can use the request parameters as selection criteria to return a subset of the objects in a bucket. | ||||
| // request parameters :- | ||||
| // --------- | ||||
| // ?continuation-token - Used to continue iterating over a set of objects | ||||
| // ?delimiter - A delimiter is a character you use to group keys. | ||||
| // ?prefix - Limits the response to keys that begin with the specified prefix. | ||||
| // ?max-keys - Sets the maximum number of keys returned in the response body. | ||||
| // ?metadata - Specifies if we want metadata for the objects as part of list operation. | ||||
| func (c Client) listObjectsV2Query(ctx context.Context, bucketName, objectPrefix, continuationToken string, fetchOwner, metadata bool, delimiter string, maxkeys int) (ListBucketV2Result, error) { | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		return ListBucketV2Result{}, err | ||||
| 	} | ||||
| 	// Validate object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(objectPrefix); err != nil { | ||||
| 		return ListBucketV2Result{}, err | ||||
| 	} | ||||
| 	// Get resources properly escaped and lined up before | ||||
| 	// using them in http request. | ||||
| 	urlValues := make(url.Values) | ||||
|  | ||||
| 	// Always set list-type in ListObjects V2 | ||||
| 	urlValues.Set("list-type", "2") | ||||
|  | ||||
| 	if metadata { | ||||
| 		urlValues.Set("metadata", "true") | ||||
| 	} | ||||
|  | ||||
| 	// Always set encoding-type in ListObjects V2 | ||||
| 	urlValues.Set("encoding-type", "url") | ||||
|  | ||||
| 	// Set object prefix, prefix value to be set to empty is okay. | ||||
| 	urlValues.Set("prefix", objectPrefix) | ||||
|  | ||||
| 	// Set delimiter, delimiter value to be set to empty is okay. | ||||
| 	urlValues.Set("delimiter", delimiter) | ||||
|  | ||||
| 	// Set continuation token | ||||
| 	if continuationToken != "" { | ||||
| 		urlValues.Set("continuation-token", continuationToken) | ||||
| 	} | ||||
|  | ||||
| 	// Fetch owner when listing | ||||
| 	if fetchOwner { | ||||
| 		urlValues.Set("fetch-owner", "true") | ||||
| 	} | ||||
|  | ||||
| 	// Set max keys. | ||||
| 	if maxkeys > 0 { | ||||
| 		urlValues.Set("max-keys", fmt.Sprintf("%d", maxkeys)) | ||||
| 	} | ||||
|  | ||||
| 	// Execute GET on bucket to list objects. | ||||
| 	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{ | ||||
| 		bucketName:       bucketName, | ||||
| 		queryValues:      urlValues, | ||||
| 		contentSHA256Hex: emptySHA256Hex, | ||||
| 	}) | ||||
| 	defer closeResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return ListBucketV2Result{}, err | ||||
| 	} | ||||
| 	if resp != nil { | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return ListBucketV2Result{}, httpRespToErrorResponse(resp, bucketName, "") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Decode listBuckets XML. | ||||
| 	listBucketResult := ListBucketV2Result{} | ||||
| 	if err = xmlDecoder(resp.Body, &listBucketResult); err != nil { | ||||
| 		return listBucketResult, err | ||||
| 	} | ||||
|  | ||||
| 	// This is an additional verification check to make | ||||
| 	// sure proper responses are received. | ||||
| 	if listBucketResult.IsTruncated && listBucketResult.NextContinuationToken == "" { | ||||
| 		return listBucketResult, ErrorResponse{ | ||||
| 			Code:    "NotImplemented", | ||||
| 			Message: "Truncated response should have continuation token set", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listBucketResult.Contents { | ||||
| 		listBucketResult.Contents[i].Key, err = decodeS3Name(obj.Key, listBucketResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listBucketResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listBucketResult.CommonPrefixes { | ||||
| 		listBucketResult.CommonPrefixes[i].Prefix, err = decodeS3Name(obj.Prefix, listBucketResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listBucketResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Success. | ||||
| 	return listBucketResult, nil | ||||
| } | ||||
|  | ||||
| func (c Client) listObjects(ctx context.Context, bucketName, objectPrefix string, recursive bool, maxKeys int) <-chan ObjectInfo { | ||||
| 	// Allocate new list objects channel. | ||||
| 	objectStatCh := make(chan ObjectInfo, 1) | ||||
| 	// Default listing is delimited at "/" | ||||
| 	delimiter := "/" | ||||
| 	if recursive { | ||||
| 		// If recursive we do not delimit. | ||||
| 		delimiter = "" | ||||
| 	} | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		defer close(objectStatCh) | ||||
| 		objectStatCh <- ObjectInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return objectStatCh | ||||
| 	} | ||||
| 	// Validate incoming object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(objectPrefix); err != nil { | ||||
| 		defer close(objectStatCh) | ||||
| 		objectStatCh <- ObjectInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return objectStatCh | ||||
| 	} | ||||
|  | ||||
| 	// Initiate list objects goroutine here. | ||||
| 	go func(objectStatCh chan<- ObjectInfo) { | ||||
| 		defer close(objectStatCh) | ||||
|  | ||||
| 		marker := "" | ||||
| 		for { | ||||
| 			// Get list of objects a maximum of 1000 per request. | ||||
| 			result, err := c.listObjectsQuery(ctx, bucketName, objectPrefix, marker, delimiter, maxKeys) | ||||
| 			if err != nil { | ||||
| 				objectStatCh <- ObjectInfo{ | ||||
| 					Err: err, | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// If contents are available loop through and send over channel. | ||||
| 			for _, object := range result.Contents { | ||||
| 				// Save the marker. | ||||
| 				marker = object.Key | ||||
| 				select { | ||||
| 				// Send object content. | ||||
| 				case objectStatCh <- object: | ||||
| 				// If receives done from the caller, return here. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Send all common prefixes if any. | ||||
| 			// NOTE: prefixes are only present if the request is delimited. | ||||
| 			for _, obj := range result.CommonPrefixes { | ||||
| 				select { | ||||
| 				// Send object prefixes. | ||||
| 				case objectStatCh <- ObjectInfo{Key: obj.Prefix}: | ||||
| 				// If receives done from the caller, return here. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// If next marker present, save it for next request. | ||||
| 			if result.NextMarker != "" { | ||||
| 				marker = result.NextMarker | ||||
| 			} | ||||
|  | ||||
| 			// Listing ends result is not truncated, return right here. | ||||
| 			if !result.IsTruncated { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}(objectStatCh) | ||||
| 	return objectStatCh | ||||
| } | ||||
|  | ||||
| func (c Client) listObjectVersions(ctx context.Context, bucketName, prefix string, recursive bool, maxKeys int) <-chan ObjectInfo { | ||||
| 	// Allocate new list objects channel. | ||||
| 	resultCh := make(chan ObjectInfo, 1) | ||||
| 	// Default listing is delimited at "/" | ||||
| 	delimiter := "/" | ||||
| 	if recursive { | ||||
| 		// If recursive we do not delimit. | ||||
| 		delimiter = "" | ||||
| 	} | ||||
|  | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		defer close(resultCh) | ||||
| 		resultCh <- ObjectInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return resultCh | ||||
| 	} | ||||
|  | ||||
| 	// Validate incoming object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(prefix); err != nil { | ||||
| 		defer close(resultCh) | ||||
| 		resultCh <- ObjectInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return resultCh | ||||
| 	} | ||||
|  | ||||
| 	// Initiate list objects goroutine here. | ||||
| 	go func(resultCh chan<- ObjectInfo) { | ||||
| 		defer close(resultCh) | ||||
|  | ||||
| 		var ( | ||||
| 			keyMarker       = "" | ||||
| 			versionIDMarker = "" | ||||
| 		) | ||||
|  | ||||
| 		for { | ||||
| 			// Get list of objects a maximum of 1000 per request. | ||||
| 			result, err := c.listObjectVersionsQuery(ctx, bucketName, prefix, keyMarker, versionIDMarker, delimiter, maxKeys) | ||||
| 			if err != nil { | ||||
| 				resultCh <- ObjectInfo{ | ||||
| 					Err: err, | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// If contents are available loop through and send over channel. | ||||
| 			for _, version := range result.Versions { | ||||
| 				info := ObjectInfo{ | ||||
| 					ETag:         trimEtag(version.ETag), | ||||
| 					Key:          version.Key, | ||||
| 					LastModified: version.LastModified, | ||||
| 					Size:         version.Size, | ||||
| 					Owner:        version.Owner, | ||||
| 					StorageClass: version.StorageClass, | ||||
| 					IsLatest:     version.IsLatest, | ||||
| 					VersionID:    version.VersionID, | ||||
|  | ||||
| 					IsDeleteMarker: version.isDeleteMarker, | ||||
| 				} | ||||
| 				select { | ||||
| 				// Send object version info. | ||||
| 				case resultCh <- info: | ||||
| 					// If receives done from the caller, return here. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Send all common prefixes if any. | ||||
| 			// NOTE: prefixes are only present if the request is delimited. | ||||
| 			for _, obj := range result.CommonPrefixes { | ||||
| 				select { | ||||
| 				// Send object prefixes. | ||||
| 				case resultCh <- ObjectInfo{Key: obj.Prefix}: | ||||
| 				// If receives done from the caller, return here. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// If next key marker is present, save it for next request. | ||||
| 			if result.NextKeyMarker != "" { | ||||
| 				keyMarker = result.NextKeyMarker | ||||
| 			} | ||||
|  | ||||
| 			// If next version id marker is present, save it for next request. | ||||
| 			if result.NextVersionIDMarker != "" { | ||||
| 				versionIDMarker = result.NextVersionIDMarker | ||||
| 			} | ||||
|  | ||||
| 			// Listing ends result is not truncated, return right here. | ||||
| 			if !result.IsTruncated { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}(resultCh) | ||||
| 	return resultCh | ||||
| } | ||||
|  | ||||
| // listObjectVersions - (List Object Versions) - List some or all (up to 1000) of the existing objects | ||||
| // and their versions in a bucket. | ||||
| // | ||||
| // You can use the request parameters as selection criteria to return a subset of the objects in a bucket. | ||||
| // request parameters :- | ||||
| // --------- | ||||
| // ?key-marker - Specifies the key to start with when listing objects in a bucket. | ||||
| // ?version-id-marker - Specifies the version id marker to start with when listing objects with versions in a bucket. | ||||
| // ?delimiter - A delimiter is a character you use to group keys. | ||||
| // ?prefix - Limits the response to keys that begin with the specified prefix. | ||||
| // ?max-keys - Sets the maximum number of keys returned in the response body. | ||||
| func (c Client) listObjectVersionsQuery(ctx context.Context, bucketName, prefix, keyMarker, versionIDMarker, delimiter string, maxkeys int) (ListVersionsResult, error) { | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		return ListVersionsResult{}, err | ||||
| 	} | ||||
| 	// Validate object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(prefix); err != nil { | ||||
| 		return ListVersionsResult{}, err | ||||
| 	} | ||||
| 	// Get resources properly escaped and lined up before | ||||
| 	// using them in http request. | ||||
| 	urlValues := make(url.Values) | ||||
|  | ||||
| 	// Set versions to trigger versioning API | ||||
| 	urlValues.Set("versions", "") | ||||
|  | ||||
| 	// Set object prefix, prefix value to be set to empty is okay. | ||||
| 	urlValues.Set("prefix", prefix) | ||||
|  | ||||
| 	// Set delimiter, delimiter value to be set to empty is okay. | ||||
| 	urlValues.Set("delimiter", delimiter) | ||||
|  | ||||
| 	// Set object marker. | ||||
| 	if keyMarker != "" { | ||||
| 		urlValues.Set("key-marker", keyMarker) | ||||
| 	} | ||||
|  | ||||
| 	// Set max keys. | ||||
| 	if maxkeys > 0 { | ||||
| 		urlValues.Set("max-keys", fmt.Sprintf("%d", maxkeys)) | ||||
| 	} | ||||
|  | ||||
| 	// Set version ID marker | ||||
| 	if versionIDMarker != "" { | ||||
| 		urlValues.Set("version-id-marker", versionIDMarker) | ||||
| 	} | ||||
|  | ||||
| 	// Always set encoding-type | ||||
| 	urlValues.Set("encoding-type", "url") | ||||
|  | ||||
| 	// Execute GET on bucket to list objects. | ||||
| 	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{ | ||||
| 		bucketName:       bucketName, | ||||
| 		queryValues:      urlValues, | ||||
| 		contentSHA256Hex: emptySHA256Hex, | ||||
| 	}) | ||||
| 	defer closeResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return ListVersionsResult{}, err | ||||
| 	} | ||||
| 	if resp != nil { | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return ListVersionsResult{}, httpRespToErrorResponse(resp, bucketName, "") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Decode ListVersionsResult XML. | ||||
| 	listObjectVersionsOutput := ListVersionsResult{} | ||||
| 	err = xmlDecoder(resp.Body, &listObjectVersionsOutput) | ||||
| 	if err != nil { | ||||
| 		return ListVersionsResult{}, err | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listObjectVersionsOutput.Versions { | ||||
| 		listObjectVersionsOutput.Versions[i].Key, err = decodeS3Name(obj.Key, listObjectVersionsOutput.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listObjectVersionsOutput, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listObjectVersionsOutput.CommonPrefixes { | ||||
| 		listObjectVersionsOutput.CommonPrefixes[i].Prefix, err = decodeS3Name(obj.Prefix, listObjectVersionsOutput.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listObjectVersionsOutput, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if listObjectVersionsOutput.NextKeyMarker != "" { | ||||
| 		listObjectVersionsOutput.NextKeyMarker, err = decodeS3Name(listObjectVersionsOutput.NextKeyMarker, listObjectVersionsOutput.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listObjectVersionsOutput, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return listObjectVersionsOutput, nil | ||||
| } | ||||
|  | ||||
| // listObjects - (List Objects) - List some or all (up to 1000) of the objects in a bucket. | ||||
| // | ||||
| // You can use the request parameters as selection criteria to return a subset of the objects in a bucket. | ||||
| // request parameters :- | ||||
| // --------- | ||||
| // ?marker - Specifies the key to start with when listing objects in a bucket. | ||||
| // ?delimiter - A delimiter is a character you use to group keys. | ||||
| // ?prefix - Limits the response to keys that begin with the specified prefix. | ||||
| // ?max-keys - Sets the maximum number of keys returned in the response body. | ||||
| func (c Client) listObjectsQuery(ctx context.Context, bucketName, objectPrefix, objectMarker, delimiter string, maxkeys int) (ListBucketResult, error) { | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		return ListBucketResult{}, err | ||||
| 	} | ||||
| 	// Validate object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(objectPrefix); err != nil { | ||||
| 		return ListBucketResult{}, err | ||||
| 	} | ||||
| 	// Get resources properly escaped and lined up before | ||||
| 	// using them in http request. | ||||
| 	urlValues := make(url.Values) | ||||
|  | ||||
| 	// Set object prefix, prefix value to be set to empty is okay. | ||||
| 	urlValues.Set("prefix", objectPrefix) | ||||
|  | ||||
| 	// Set delimiter, delimiter value to be set to empty is okay. | ||||
| 	urlValues.Set("delimiter", delimiter) | ||||
|  | ||||
| 	// Set object marker. | ||||
| 	if objectMarker != "" { | ||||
| 		urlValues.Set("marker", objectMarker) | ||||
| 	} | ||||
|  | ||||
| 	// Set max keys. | ||||
| 	if maxkeys > 0 { | ||||
| 		urlValues.Set("max-keys", fmt.Sprintf("%d", maxkeys)) | ||||
| 	} | ||||
|  | ||||
| 	// Always set encoding-type | ||||
| 	urlValues.Set("encoding-type", "url") | ||||
|  | ||||
| 	// Execute GET on bucket to list objects. | ||||
| 	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{ | ||||
| 		bucketName:       bucketName, | ||||
| 		queryValues:      urlValues, | ||||
| 		contentSHA256Hex: emptySHA256Hex, | ||||
| 	}) | ||||
| 	defer closeResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return ListBucketResult{}, err | ||||
| 	} | ||||
| 	if resp != nil { | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return ListBucketResult{}, httpRespToErrorResponse(resp, bucketName, "") | ||||
| 		} | ||||
| 	} | ||||
| 	// Decode listBuckets XML. | ||||
| 	listBucketResult := ListBucketResult{} | ||||
| 	err = xmlDecoder(resp.Body, &listBucketResult) | ||||
| 	if err != nil { | ||||
| 		return listBucketResult, err | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listBucketResult.Contents { | ||||
| 		listBucketResult.Contents[i].Key, err = decodeS3Name(obj.Key, listBucketResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listBucketResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listBucketResult.CommonPrefixes { | ||||
| 		listBucketResult.CommonPrefixes[i].Prefix, err = decodeS3Name(obj.Prefix, listBucketResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listBucketResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if listBucketResult.NextMarker != "" { | ||||
| 		listBucketResult.NextMarker, err = decodeS3Name(listBucketResult.NextMarker, listBucketResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listBucketResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return listBucketResult, nil | ||||
| } | ||||
|  | ||||
| // ListObjectsOptions holds all options of a list object request | ||||
| type ListObjectsOptions struct { | ||||
| 	// Include objects versions in the listing | ||||
| 	WithVersions bool | ||||
| 	// Include objects metadata in the listing | ||||
| 	WithMetadata bool | ||||
| 	// Only list objects with the prefix | ||||
| 	Prefix string | ||||
| 	// Ignore '/' delimiter | ||||
| 	Recursive bool | ||||
| 	// The maximum number of objects requested per | ||||
| 	// batch, advanced use-case not useful for most | ||||
| 	// applications | ||||
| 	MaxKeys int | ||||
|  | ||||
| 	// Use the deprecated list objects V1 API | ||||
| 	UseV1 bool | ||||
| } | ||||
|  | ||||
| // ListObjects returns objects list after evaluating the passed options. | ||||
| // | ||||
| //   api := client.New(....) | ||||
| //   for object := range api.ListObjects(ctx, "mytestbucket", minio.ListObjectsOptions{Prefix: "starthere", Recursive:true}) { | ||||
| //       fmt.Println(object) | ||||
| //   } | ||||
| // | ||||
| func (c Client) ListObjects(ctx context.Context, bucketName string, opts ListObjectsOptions) <-chan ObjectInfo { | ||||
| 	if opts.WithVersions { | ||||
| 		return c.listObjectVersions(ctx, bucketName, opts.Prefix, opts.Recursive, opts.MaxKeys) | ||||
| 	} | ||||
|  | ||||
| 	// Use legacy list objects v1 API | ||||
| 	if opts.UseV1 { | ||||
| 		return c.listObjects(ctx, bucketName, opts.Prefix, opts.Recursive, opts.MaxKeys) | ||||
| 	} | ||||
|  | ||||
| 	// Check whether this is snowball region, if yes ListObjectsV2 doesn't work, fallback to listObjectsV1. | ||||
| 	if location, ok := c.bucketLocCache.Get(bucketName); ok { | ||||
| 		if location == "snowball" { | ||||
| 			return c.listObjects(ctx, bucketName, opts.Prefix, opts.Recursive, opts.MaxKeys) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return c.listObjectsV2(ctx, bucketName, opts.Prefix, opts.Recursive, opts.WithMetadata, opts.MaxKeys) | ||||
| } | ||||
|  | ||||
| // ListIncompleteUploads - List incompletely uploaded multipart objects. | ||||
| // | ||||
| // ListIncompleteUploads lists all incompleted objects matching the | ||||
| // objectPrefix from the specified bucket. If recursion is enabled | ||||
| // it would list all subdirectories and all its contents. | ||||
| // | ||||
| // Your input parameters are just bucketName, objectPrefix, recursive. | ||||
| // If you enable recursive as 'true' this function will return back all | ||||
| // the multipart objects in a given bucket name. | ||||
| // | ||||
| //   api := client.New(....) | ||||
| //   // Recurively list all objects in 'mytestbucket' | ||||
| //   recursive := true | ||||
| //   for message := range api.ListIncompleteUploads(context.Background(), "mytestbucket", "starthere", recursive) { | ||||
| //       fmt.Println(message) | ||||
| //   } | ||||
| func (c Client) ListIncompleteUploads(ctx context.Context, bucketName, objectPrefix string, recursive bool) <-chan ObjectMultipartInfo { | ||||
| 	return c.listIncompleteUploads(ctx, bucketName, objectPrefix, recursive) | ||||
| } | ||||
|  | ||||
| // listIncompleteUploads lists all incomplete uploads. | ||||
| func (c Client) listIncompleteUploads(ctx context.Context, bucketName, objectPrefix string, recursive bool) <-chan ObjectMultipartInfo { | ||||
| 	// Allocate channel for multipart uploads. | ||||
| 	objectMultipartStatCh := make(chan ObjectMultipartInfo, 1) | ||||
| 	// Delimiter is set to "/" by default. | ||||
| 	delimiter := "/" | ||||
| 	if recursive { | ||||
| 		// If recursive do not delimit. | ||||
| 		delimiter = "" | ||||
| 	} | ||||
| 	// Validate bucket name. | ||||
| 	if err := s3utils.CheckValidBucketName(bucketName); err != nil { | ||||
| 		defer close(objectMultipartStatCh) | ||||
| 		objectMultipartStatCh <- ObjectMultipartInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return objectMultipartStatCh | ||||
| 	} | ||||
| 	// Validate incoming object prefix. | ||||
| 	if err := s3utils.CheckValidObjectNamePrefix(objectPrefix); err != nil { | ||||
| 		defer close(objectMultipartStatCh) | ||||
| 		objectMultipartStatCh <- ObjectMultipartInfo{ | ||||
| 			Err: err, | ||||
| 		} | ||||
| 		return objectMultipartStatCh | ||||
| 	} | ||||
| 	go func(objectMultipartStatCh chan<- ObjectMultipartInfo) { | ||||
| 		defer close(objectMultipartStatCh) | ||||
| 		// object and upload ID marker for future requests. | ||||
| 		var objectMarker string | ||||
| 		var uploadIDMarker string | ||||
| 		for { | ||||
| 			// list all multipart uploads. | ||||
| 			result, err := c.listMultipartUploadsQuery(ctx, bucketName, objectMarker, uploadIDMarker, objectPrefix, delimiter, 0) | ||||
| 			if err != nil { | ||||
| 				objectMultipartStatCh <- ObjectMultipartInfo{ | ||||
| 					Err: err, | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
| 			objectMarker = result.NextKeyMarker | ||||
| 			uploadIDMarker = result.NextUploadIDMarker | ||||
|  | ||||
| 			// Send all multipart uploads. | ||||
| 			for _, obj := range result.Uploads { | ||||
| 				// Calculate total size of the uploaded parts if 'aggregateSize' is enabled. | ||||
| 				select { | ||||
| 				// Send individual uploads here. | ||||
| 				case objectMultipartStatCh <- obj: | ||||
| 				// If the context is canceled | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 			// Send all common prefixes if any. | ||||
| 			// NOTE: prefixes are only present if the request is delimited. | ||||
| 			for _, obj := range result.CommonPrefixes { | ||||
| 				select { | ||||
| 				// Send delimited prefixes here. | ||||
| 				case objectMultipartStatCh <- ObjectMultipartInfo{Key: obj.Prefix, Size: 0}: | ||||
| 				// If context is canceled. | ||||
| 				case <-ctx.Done(): | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 			// Listing ends if result not truncated, return right here. | ||||
| 			if !result.IsTruncated { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}(objectMultipartStatCh) | ||||
| 	// return. | ||||
| 	return objectMultipartStatCh | ||||
|  | ||||
| } | ||||
|  | ||||
| // listMultipartUploadsQuery - (List Multipart Uploads). | ||||
| //   - Lists some or all (up to 1000) in-progress multipart uploads in a bucket. | ||||
| // | ||||
| // You can use the request parameters as selection criteria to return a subset of the uploads in a bucket. | ||||
| // request parameters. :- | ||||
| // --------- | ||||
| // ?key-marker - Specifies the multipart upload after which listing should begin. | ||||
| // ?upload-id-marker - Together with key-marker specifies the multipart upload after which listing should begin. | ||||
| // ?delimiter - A delimiter is a character you use to group keys. | ||||
| // ?prefix - Limits the response to keys that begin with the specified prefix. | ||||
| // ?max-uploads - Sets the maximum number of multipart uploads returned in the response body. | ||||
| func (c Client) listMultipartUploadsQuery(ctx context.Context, bucketName, keyMarker, uploadIDMarker, prefix, delimiter string, maxUploads int) (ListMultipartUploadsResult, error) { | ||||
| 	// Get resources properly escaped and lined up before using them in http request. | ||||
| 	urlValues := make(url.Values) | ||||
| 	// Set uploads. | ||||
| 	urlValues.Set("uploads", "") | ||||
| 	// Set object key marker. | ||||
| 	if keyMarker != "" { | ||||
| 		urlValues.Set("key-marker", keyMarker) | ||||
| 	} | ||||
| 	// Set upload id marker. | ||||
| 	if uploadIDMarker != "" { | ||||
| 		urlValues.Set("upload-id-marker", uploadIDMarker) | ||||
| 	} | ||||
|  | ||||
| 	// Set object prefix, prefix value to be set to empty is okay. | ||||
| 	urlValues.Set("prefix", prefix) | ||||
|  | ||||
| 	// Set delimiter, delimiter value to be set to empty is okay. | ||||
| 	urlValues.Set("delimiter", delimiter) | ||||
|  | ||||
| 	// Always set encoding-type | ||||
| 	urlValues.Set("encoding-type", "url") | ||||
|  | ||||
| 	// maxUploads should be 1000 or less. | ||||
| 	if maxUploads > 0 { | ||||
| 		// Set max-uploads. | ||||
| 		urlValues.Set("max-uploads", fmt.Sprintf("%d", maxUploads)) | ||||
| 	} | ||||
|  | ||||
| 	// Execute GET on bucketName to list multipart uploads. | ||||
| 	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{ | ||||
| 		bucketName:       bucketName, | ||||
| 		queryValues:      urlValues, | ||||
| 		contentSHA256Hex: emptySHA256Hex, | ||||
| 	}) | ||||
| 	defer closeResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return ListMultipartUploadsResult{}, err | ||||
| 	} | ||||
| 	if resp != nil { | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return ListMultipartUploadsResult{}, httpRespToErrorResponse(resp, bucketName, "") | ||||
| 		} | ||||
| 	} | ||||
| 	// Decode response body. | ||||
| 	listMultipartUploadsResult := ListMultipartUploadsResult{} | ||||
| 	err = xmlDecoder(resp.Body, &listMultipartUploadsResult) | ||||
| 	if err != nil { | ||||
| 		return listMultipartUploadsResult, err | ||||
| 	} | ||||
|  | ||||
| 	listMultipartUploadsResult.NextKeyMarker, err = decodeS3Name(listMultipartUploadsResult.NextKeyMarker, listMultipartUploadsResult.EncodingType) | ||||
| 	if err != nil { | ||||
| 		return listMultipartUploadsResult, err | ||||
| 	} | ||||
|  | ||||
| 	listMultipartUploadsResult.NextUploadIDMarker, err = decodeS3Name(listMultipartUploadsResult.NextUploadIDMarker, listMultipartUploadsResult.EncodingType) | ||||
| 	if err != nil { | ||||
| 		return listMultipartUploadsResult, err | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listMultipartUploadsResult.Uploads { | ||||
| 		listMultipartUploadsResult.Uploads[i].Key, err = decodeS3Name(obj.Key, listMultipartUploadsResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listMultipartUploadsResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i, obj := range listMultipartUploadsResult.CommonPrefixes { | ||||
| 		listMultipartUploadsResult.CommonPrefixes[i].Prefix, err = decodeS3Name(obj.Prefix, listMultipartUploadsResult.EncodingType) | ||||
| 		if err != nil { | ||||
| 			return listMultipartUploadsResult, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return listMultipartUploadsResult, nil | ||||
| } | ||||
|  | ||||
| // listObjectParts list all object parts recursively. | ||||
| func (c Client) listObjectParts(ctx context.Context, bucketName, objectName, uploadID string) (partsInfo map[int]ObjectPart, err error) { | ||||
| 	// Part number marker for the next batch of request. | ||||
| 	var nextPartNumberMarker int | ||||
| 	partsInfo = make(map[int]ObjectPart) | ||||
| 	for { | ||||
| 		// Get list of uploaded parts a maximum of 1000 per request. | ||||
| 		listObjPartsResult, err := c.listObjectPartsQuery(ctx, bucketName, objectName, uploadID, nextPartNumberMarker, 1000) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// Append to parts info. | ||||
| 		for _, part := range listObjPartsResult.ObjectParts { | ||||
| 			// Trim off the odd double quotes from ETag in the beginning and end. | ||||
| 			part.ETag = trimEtag(part.ETag) | ||||
| 			partsInfo[part.PartNumber] = part | ||||
| 		} | ||||
| 		// Keep part number marker, for the next iteration. | ||||
| 		nextPartNumberMarker = listObjPartsResult.NextPartNumberMarker | ||||
| 		// Listing ends result is not truncated, return right here. | ||||
| 		if !listObjPartsResult.IsTruncated { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Return all the parts. | ||||
| 	return partsInfo, nil | ||||
| } | ||||
|  | ||||
| // findUploadIDs lists all incomplete uploads and find the uploadIDs of the matching object name. | ||||
| func (c Client) findUploadIDs(ctx context.Context, bucketName, objectName string) ([]string, error) { | ||||
| 	var uploadIDs []string | ||||
| 	// Make list incomplete uploads recursive. | ||||
| 	isRecursive := true | ||||
| 	// List all incomplete uploads. | ||||
| 	for mpUpload := range c.listIncompleteUploads(ctx, bucketName, objectName, isRecursive) { | ||||
| 		if mpUpload.Err != nil { | ||||
| 			return nil, mpUpload.Err | ||||
| 		} | ||||
| 		if objectName == mpUpload.Key { | ||||
| 			uploadIDs = append(uploadIDs, mpUpload.UploadID) | ||||
| 		} | ||||
| 	} | ||||
| 	// Return the latest upload id. | ||||
| 	return uploadIDs, nil | ||||
| } | ||||
|  | ||||
| // listObjectPartsQuery (List Parts query) | ||||
| //     - lists some or all (up to 1000) parts that have been uploaded | ||||
| //     for a specific multipart upload | ||||
| // | ||||
| // You can use the request parameters as selection criteria to return | ||||
| // a subset of the uploads in a bucket, request parameters :- | ||||
| // --------- | ||||
| // ?part-number-marker - Specifies the part after which listing should | ||||
| // begin. | ||||
| // ?max-parts - Maximum parts to be listed per request. | ||||
| func (c Client) listObjectPartsQuery(ctx context.Context, bucketName, objectName, uploadID string, partNumberMarker, maxParts int) (ListObjectPartsResult, error) { | ||||
| 	// Get resources properly escaped and lined up before using them in http request. | ||||
| 	urlValues := make(url.Values) | ||||
| 	// Set part number marker. | ||||
| 	urlValues.Set("part-number-marker", fmt.Sprintf("%d", partNumberMarker)) | ||||
| 	// Set upload id. | ||||
| 	urlValues.Set("uploadId", uploadID) | ||||
|  | ||||
| 	// maxParts should be 1000 or less. | ||||
| 	if maxParts > 0 { | ||||
| 		// Set max parts. | ||||
| 		urlValues.Set("max-parts", fmt.Sprintf("%d", maxParts)) | ||||
| 	} | ||||
|  | ||||
| 	// Execute GET on objectName to get list of parts. | ||||
| 	resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{ | ||||
| 		bucketName:       bucketName, | ||||
| 		objectName:       objectName, | ||||
| 		queryValues:      urlValues, | ||||
| 		contentSHA256Hex: emptySHA256Hex, | ||||
| 	}) | ||||
| 	defer closeResponse(resp) | ||||
| 	if err != nil { | ||||
| 		return ListObjectPartsResult{}, err | ||||
| 	} | ||||
| 	if resp != nil { | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return ListObjectPartsResult{}, httpRespToErrorResponse(resp, bucketName, objectName) | ||||
| 		} | ||||
| 	} | ||||
| 	// Decode list object parts XML. | ||||
| 	listObjectPartsResult := ListObjectPartsResult{} | ||||
| 	err = xmlDecoder(resp.Body, &listObjectPartsResult) | ||||
| 	if err != nil { | ||||
| 		return listObjectPartsResult, err | ||||
| 	} | ||||
| 	return listObjectPartsResult, nil | ||||
| } | ||||
|  | ||||
| // Decode an S3 object name according to the encoding type | ||||
| func decodeS3Name(name, encodingType string) (string, error) { | ||||
| 	switch encodingType { | ||||
| 	case "url": | ||||
| 		return url.QueryUnescape(name) | ||||
| 	default: | ||||
| 		return name, nil | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user