2021-04-18 15:41:13 -04:00
|
|
|
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
|
|
//
|
|
|
|
// This file is part of MinIO Object Storage stack
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2017-12-05 20:58:09 -05:00
|
|
|
|
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
2019-11-01 19:58:11 -04:00
|
|
|
"context"
|
2020-11-19 13:38:02 -05:00
|
|
|
"net"
|
2017-12-05 20:58:09 -05:00
|
|
|
"net/http"
|
2019-01-05 17:16:43 -05:00
|
|
|
"strings"
|
2019-11-01 19:58:11 -04:00
|
|
|
"time"
|
2017-12-05 20:58:09 -05:00
|
|
|
|
2019-10-04 13:35:33 -04:00
|
|
|
"github.com/minio/minio/cmd/config"
|
2019-07-03 01:34:32 -04:00
|
|
|
xhttp "github.com/minio/minio/cmd/http"
|
2019-01-05 17:16:43 -05:00
|
|
|
"github.com/minio/minio/cmd/logger"
|
2019-10-04 13:35:33 -04:00
|
|
|
"github.com/minio/minio/pkg/env"
|
2017-12-05 20:58:09 -05:00
|
|
|
"github.com/minio/minio/pkg/hash"
|
2019-09-12 19:44:51 -04:00
|
|
|
xnet "github.com/minio/minio/pkg/net"
|
2017-12-05 20:58:09 -05:00
|
|
|
|
2020-07-14 12:38:05 -04:00
|
|
|
minio "github.com/minio/minio-go/v7"
|
2017-12-05 20:58:09 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// CanonicalizeETag provides canonicalizeETag function alias.
|
|
|
|
CanonicalizeETag = canonicalizeETag
|
|
|
|
|
|
|
|
// MustGetUUID function alias.
|
|
|
|
MustGetUUID = mustGetUUID
|
2019-01-05 17:16:43 -05:00
|
|
|
|
|
|
|
// CleanMetadataKeys provides cleanMetadataKeys function alias.
|
|
|
|
CleanMetadataKeys = cleanMetadataKeys
|
2019-04-17 12:52:08 -04:00
|
|
|
|
|
|
|
// PathJoin function alias.
|
|
|
|
PathJoin = pathJoin
|
|
|
|
|
|
|
|
// ListObjects function alias.
|
|
|
|
ListObjects = listObjects
|
|
|
|
|
2020-08-25 15:26:48 -04:00
|
|
|
// FilterListEntries function alias.
|
|
|
|
FilterListEntries = filterListEntries
|
2019-04-17 12:52:08 -04:00
|
|
|
|
|
|
|
// IsStringEqual is string equal.
|
|
|
|
IsStringEqual = isStringEqual
|
2017-12-05 20:58:09 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// FromMinioClientMetadata converts minio metadata to map[string]string
|
|
|
|
func FromMinioClientMetadata(metadata map[string][]string) map[string]string {
|
2020-09-10 14:37:22 -04:00
|
|
|
mm := make(map[string]string, len(metadata))
|
2017-12-05 20:58:09 -05:00
|
|
|
for k, v := range metadata {
|
|
|
|
mm[http.CanonicalHeaderKey(k)] = v[0]
|
|
|
|
}
|
|
|
|
return mm
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientObjectPart converts minio ObjectPart to PartInfo
|
|
|
|
func FromMinioClientObjectPart(op minio.ObjectPart) PartInfo {
|
|
|
|
return PartInfo{
|
|
|
|
Size: op.Size,
|
|
|
|
ETag: canonicalizeETag(op.ETag),
|
|
|
|
LastModified: op.LastModified,
|
|
|
|
PartNumber: op.PartNumber,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientListPartsInfo converts minio ListObjectPartsResult to ListPartsInfo
|
|
|
|
func FromMinioClientListPartsInfo(lopr minio.ListObjectPartsResult) ListPartsInfo {
|
|
|
|
// Convert minio ObjectPart to PartInfo
|
|
|
|
fromMinioClientObjectParts := func(parts []minio.ObjectPart) []PartInfo {
|
|
|
|
toParts := make([]PartInfo, len(parts))
|
|
|
|
for i, part := range parts {
|
|
|
|
toParts[i] = FromMinioClientObjectPart(part)
|
|
|
|
}
|
|
|
|
return toParts
|
|
|
|
}
|
|
|
|
|
|
|
|
return ListPartsInfo{
|
|
|
|
UploadID: lopr.UploadID,
|
|
|
|
Bucket: lopr.Bucket,
|
|
|
|
Object: lopr.Key,
|
|
|
|
StorageClass: "",
|
|
|
|
PartNumberMarker: lopr.PartNumberMarker,
|
|
|
|
NextPartNumberMarker: lopr.NextPartNumberMarker,
|
|
|
|
MaxParts: lopr.MaxParts,
|
|
|
|
IsTruncated: lopr.IsTruncated,
|
|
|
|
Parts: fromMinioClientObjectParts(lopr.ObjectParts),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientListMultipartsInfo converts minio ListMultipartUploadsResult to ListMultipartsInfo
|
|
|
|
func FromMinioClientListMultipartsInfo(lmur minio.ListMultipartUploadsResult) ListMultipartsInfo {
|
|
|
|
uploads := make([]MultipartInfo, len(lmur.Uploads))
|
|
|
|
|
|
|
|
for i, um := range lmur.Uploads {
|
|
|
|
uploads[i] = MultipartInfo{
|
|
|
|
Object: um.Key,
|
|
|
|
UploadID: um.UploadID,
|
|
|
|
Initiated: um.Initiated,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
commonPrefixes := make([]string, len(lmur.CommonPrefixes))
|
|
|
|
for i, cp := range lmur.CommonPrefixes {
|
|
|
|
commonPrefixes[i] = cp.Prefix
|
|
|
|
}
|
|
|
|
|
|
|
|
return ListMultipartsInfo{
|
|
|
|
KeyMarker: lmur.KeyMarker,
|
|
|
|
UploadIDMarker: lmur.UploadIDMarker,
|
|
|
|
NextKeyMarker: lmur.NextKeyMarker,
|
|
|
|
NextUploadIDMarker: lmur.NextUploadIDMarker,
|
|
|
|
MaxUploads: int(lmur.MaxUploads),
|
|
|
|
IsTruncated: lmur.IsTruncated,
|
|
|
|
Uploads: uploads,
|
|
|
|
Prefix: lmur.Prefix,
|
|
|
|
Delimiter: lmur.Delimiter,
|
|
|
|
CommonPrefixes: commonPrefixes,
|
|
|
|
EncodingType: lmur.EncodingType,
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientObjectInfo converts minio ObjectInfo to gateway ObjectInfo
|
|
|
|
func FromMinioClientObjectInfo(bucket string, oi minio.ObjectInfo) ObjectInfo {
|
|
|
|
userDefined := FromMinioClientMetadata(oi.Metadata)
|
2019-07-03 01:34:32 -04:00
|
|
|
userDefined[xhttp.ContentType] = oi.ContentType
|
2017-12-05 20:58:09 -05:00
|
|
|
|
|
|
|
return ObjectInfo{
|
|
|
|
Bucket: bucket,
|
|
|
|
Name: oi.Key,
|
|
|
|
ModTime: oi.LastModified,
|
|
|
|
Size: oi.Size,
|
|
|
|
ETag: canonicalizeETag(oi.ETag),
|
|
|
|
UserDefined: userDefined,
|
|
|
|
ContentType: oi.ContentType,
|
2019-07-03 01:34:32 -04:00
|
|
|
ContentEncoding: oi.Metadata.Get(xhttp.ContentEncoding),
|
2018-06-19 14:22:08 -04:00
|
|
|
StorageClass: oi.StorageClass,
|
2019-02-28 14:01:25 -05:00
|
|
|
Expires: oi.Expires,
|
2017-12-05 20:58:09 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientListBucketV2Result converts minio ListBucketResult to ListObjectsInfo
|
|
|
|
func FromMinioClientListBucketV2Result(bucket string, result minio.ListBucketV2Result) ListObjectsV2Info {
|
|
|
|
objects := make([]ObjectInfo, len(result.Contents))
|
|
|
|
|
|
|
|
for i, oi := range result.Contents {
|
|
|
|
objects[i] = FromMinioClientObjectInfo(bucket, oi)
|
|
|
|
}
|
|
|
|
|
|
|
|
prefixes := make([]string, len(result.CommonPrefixes))
|
|
|
|
for i, p := range result.CommonPrefixes {
|
|
|
|
prefixes[i] = p.Prefix
|
|
|
|
}
|
|
|
|
|
|
|
|
return ListObjectsV2Info{
|
|
|
|
IsTruncated: result.IsTruncated,
|
|
|
|
Prefixes: prefixes,
|
|
|
|
Objects: objects,
|
|
|
|
|
|
|
|
ContinuationToken: result.ContinuationToken,
|
|
|
|
NextContinuationToken: result.NextContinuationToken,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientListBucketResult converts minio ListBucketResult to ListObjectsInfo
|
|
|
|
func FromMinioClientListBucketResult(bucket string, result minio.ListBucketResult) ListObjectsInfo {
|
|
|
|
objects := make([]ObjectInfo, len(result.Contents))
|
|
|
|
|
|
|
|
for i, oi := range result.Contents {
|
|
|
|
objects[i] = FromMinioClientObjectInfo(bucket, oi)
|
|
|
|
}
|
|
|
|
|
|
|
|
prefixes := make([]string, len(result.CommonPrefixes))
|
|
|
|
for i, p := range result.CommonPrefixes {
|
|
|
|
prefixes[i] = p.Prefix
|
|
|
|
}
|
|
|
|
|
|
|
|
return ListObjectsInfo{
|
|
|
|
IsTruncated: result.IsTruncated,
|
|
|
|
NextMarker: result.NextMarker,
|
|
|
|
Prefixes: prefixes,
|
|
|
|
Objects: objects,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromMinioClientListBucketResultToV2Info converts minio ListBucketResult to ListObjectsV2Info
|
|
|
|
func FromMinioClientListBucketResultToV2Info(bucket string, result minio.ListBucketResult) ListObjectsV2Info {
|
|
|
|
objects := make([]ObjectInfo, len(result.Contents))
|
|
|
|
|
|
|
|
for i, oi := range result.Contents {
|
|
|
|
objects[i] = FromMinioClientObjectInfo(bucket, oi)
|
|
|
|
}
|
|
|
|
|
|
|
|
prefixes := make([]string, len(result.CommonPrefixes))
|
|
|
|
for i, p := range result.CommonPrefixes {
|
|
|
|
prefixes[i] = p.Prefix
|
|
|
|
}
|
|
|
|
|
|
|
|
return ListObjectsV2Info{
|
|
|
|
IsTruncated: result.IsTruncated,
|
|
|
|
Prefixes: prefixes,
|
|
|
|
Objects: objects,
|
|
|
|
ContinuationToken: result.Marker,
|
|
|
|
NextContinuationToken: result.NextMarker,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-12 15:25:59 -04:00
|
|
|
// ToMinioClientObjectInfoMetadata convertes metadata to map[string][]string
|
|
|
|
func ToMinioClientObjectInfoMetadata(metadata map[string]string) map[string][]string {
|
|
|
|
mm := make(map[string][]string, len(metadata))
|
|
|
|
for k, v := range metadata {
|
|
|
|
mm[http.CanonicalHeaderKey(k)] = []string{v}
|
|
|
|
}
|
|
|
|
return mm
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToMinioClientMetadata converts metadata to map[string]string
|
2017-12-05 20:58:09 -05:00
|
|
|
func ToMinioClientMetadata(metadata map[string]string) map[string]string {
|
2020-09-10 14:37:22 -04:00
|
|
|
mm := make(map[string]string, len(metadata))
|
2017-12-05 20:58:09 -05:00
|
|
|
for k, v := range metadata {
|
|
|
|
mm[http.CanonicalHeaderKey(k)] = v
|
|
|
|
}
|
|
|
|
return mm
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToMinioClientCompletePart converts CompletePart to minio CompletePart
|
|
|
|
func ToMinioClientCompletePart(part CompletePart) minio.CompletePart {
|
|
|
|
return minio.CompletePart{
|
|
|
|
ETag: part.ETag,
|
|
|
|
PartNumber: part.PartNumber,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToMinioClientCompleteParts converts []CompletePart to minio []CompletePart
|
|
|
|
func ToMinioClientCompleteParts(parts []CompletePart) []minio.CompletePart {
|
|
|
|
mparts := make([]minio.CompletePart, len(parts))
|
|
|
|
for i, part := range parts {
|
|
|
|
mparts[i] = ToMinioClientCompletePart(part)
|
|
|
|
}
|
|
|
|
return mparts
|
|
|
|
}
|
|
|
|
|
2019-11-01 19:58:11 -04:00
|
|
|
// IsBackendOnline - verifies if the backend is reachable
|
|
|
|
// by performing a GET request on the URL. returns 'true'
|
|
|
|
// if backend is reachable.
|
2020-11-19 13:38:02 -05:00
|
|
|
func IsBackendOnline(ctx context.Context, host string) bool {
|
|
|
|
var d net.Dialer
|
|
|
|
|
2019-11-01 19:58:11 -04:00
|
|
|
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
|
2020-11-19 13:38:02 -05:00
|
|
|
conn, err := d.DialContext(ctx, "tcp", host)
|
2019-11-01 19:58:11 -04:00
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
2020-11-19 13:38:02 -05:00
|
|
|
|
|
|
|
conn.Close()
|
2019-11-01 19:58:11 -04:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-04-09 14:39:42 -04:00
|
|
|
// ErrorRespToObjectError converts MinIO errors to minio object layer errors.
|
2017-12-05 20:58:09 -05:00
|
|
|
func ErrorRespToObjectError(err error, params ...string) error {
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket := ""
|
|
|
|
object := ""
|
|
|
|
if len(params) >= 1 {
|
|
|
|
bucket = params[0]
|
|
|
|
}
|
|
|
|
if len(params) == 2 {
|
|
|
|
object = params[1]
|
|
|
|
}
|
|
|
|
|
2020-10-29 12:52:11 -04:00
|
|
|
if xnet.IsNetworkOrHostDown(err, false) {
|
2018-04-05 18:04:40 -04:00
|
|
|
return BackendDown{}
|
2018-03-28 17:14:06 -04:00
|
|
|
}
|
|
|
|
|
2017-12-05 20:58:09 -05:00
|
|
|
minioErr, ok := err.(minio.ErrorResponse)
|
|
|
|
if !ok {
|
2019-04-09 14:39:42 -04:00
|
|
|
// We don't interpret non MinIO errors. As minio errors will
|
2017-12-05 20:58:09 -05:00
|
|
|
// have StatusCode to help to convert to object errors.
|
2018-04-05 18:04:40 -04:00
|
|
|
return err
|
2017-12-05 20:58:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
switch minioErr.Code {
|
|
|
|
case "BucketAlreadyOwnedByYou":
|
|
|
|
err = BucketAlreadyOwnedByYou{}
|
|
|
|
case "BucketNotEmpty":
|
|
|
|
err = BucketNotEmpty{}
|
|
|
|
case "NoSuchBucketPolicy":
|
2018-04-24 18:53:30 -04:00
|
|
|
err = BucketPolicyNotFound{}
|
2020-02-08 06:16:59 -05:00
|
|
|
case "NoSuchLifecycleConfiguration":
|
2019-07-19 16:20:33 -04:00
|
|
|
err = BucketLifecycleNotFound{}
|
2017-12-05 20:58:09 -05:00
|
|
|
case "InvalidBucketName":
|
|
|
|
err = BucketNameInvalid{Bucket: bucket}
|
2018-10-17 12:20:58 -04:00
|
|
|
case "InvalidPart":
|
|
|
|
err = InvalidPart{}
|
2017-12-05 20:58:09 -05:00
|
|
|
case "NoSuchBucket":
|
|
|
|
err = BucketNotFound{Bucket: bucket}
|
|
|
|
case "NoSuchKey":
|
|
|
|
if object != "" {
|
|
|
|
err = ObjectNotFound{Bucket: bucket, Object: object}
|
|
|
|
} else {
|
|
|
|
err = BucketNotFound{Bucket: bucket}
|
|
|
|
}
|
|
|
|
case "XMinioInvalidObjectName":
|
|
|
|
err = ObjectNameInvalid{}
|
|
|
|
case "AccessDenied":
|
|
|
|
err = PrefixAccessDenied{
|
|
|
|
Bucket: bucket,
|
|
|
|
Object: object,
|
|
|
|
}
|
|
|
|
case "XAmzContentSHA256Mismatch":
|
|
|
|
err = hash.SHA256Mismatch{}
|
|
|
|
case "NoSuchUpload":
|
|
|
|
err = InvalidUploadID{}
|
|
|
|
case "EntityTooSmall":
|
|
|
|
err = PartTooSmall{}
|
|
|
|
}
|
|
|
|
|
2018-04-05 18:04:40 -04:00
|
|
|
return err
|
2017-12-05 20:58:09 -05:00
|
|
|
}
|
2019-01-05 17:16:43 -05:00
|
|
|
|
2019-04-18 02:50:25 -04:00
|
|
|
// ComputeCompleteMultipartMD5 calculates MD5 ETag for complete multipart responses
|
2019-05-08 21:35:40 -04:00
|
|
|
func ComputeCompleteMultipartMD5(parts []CompletePart) string {
|
|
|
|
return getCompleteMultipartMD5(parts)
|
2019-04-18 02:50:25 -04:00
|
|
|
}
|
|
|
|
|
2019-01-05 17:16:43 -05:00
|
|
|
// parse gateway sse env variable
|
|
|
|
func parseGatewaySSE(s string) (gatewaySSE, error) {
|
|
|
|
l := strings.Split(s, ";")
|
2020-09-15 16:57:15 -04:00
|
|
|
var gwSlice gatewaySSE
|
2019-01-05 17:16:43 -05:00
|
|
|
for _, val := range l {
|
|
|
|
v := strings.ToUpper(val)
|
2020-09-15 16:57:15 -04:00
|
|
|
switch v {
|
|
|
|
case "":
|
|
|
|
continue
|
|
|
|
case gatewaySSES3:
|
|
|
|
fallthrough
|
|
|
|
case gatewaySSEC:
|
2019-01-05 17:16:43 -05:00
|
|
|
gwSlice = append(gwSlice, v)
|
|
|
|
continue
|
2020-09-15 16:57:15 -04:00
|
|
|
default:
|
|
|
|
return nil, config.ErrInvalidGWSSEValue(nil).Msg("gateway SSE cannot be (%s) ", v)
|
2019-01-05 17:16:43 -05:00
|
|
|
}
|
|
|
|
}
|
2020-09-15 16:57:15 -04:00
|
|
|
return gwSlice, nil
|
2019-01-05 17:16:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// handle gateway env vars
|
2019-11-01 18:53:16 -04:00
|
|
|
func gatewayHandleEnvVars() {
|
|
|
|
// Handle common env vars.
|
|
|
|
handleCommonEnvVars()
|
|
|
|
|
|
|
|
if !globalActiveCred.IsValid() {
|
|
|
|
logger.Fatal(config.ErrInvalidCredentials(nil),
|
2019-10-23 01:59:13 -04:00
|
|
|
"Unable to validate credentials inherited from the shell environment")
|
|
|
|
}
|
2019-10-31 02:39:09 -04:00
|
|
|
|
|
|
|
gwsseVal := env.Get("MINIO_GATEWAY_SSE", "")
|
2020-09-15 16:57:15 -04:00
|
|
|
if gwsseVal != "" {
|
2019-10-31 02:39:09 -04:00
|
|
|
var err error
|
|
|
|
GlobalGatewaySSE, err = parseGatewaySSE(gwsseVal)
|
|
|
|
if err != nil {
|
|
|
|
logger.Fatal(err, "Unable to parse MINIO_GATEWAY_SSE value (`%s`)", gwsseVal)
|
|
|
|
}
|
|
|
|
}
|
2019-01-05 17:16:43 -05:00
|
|
|
}
|
2020-02-11 10:38:01 -05:00
|
|
|
|
|
|
|
// shouldMeterRequest checks whether incoming request should be added to prometheus gateway metrics
|
|
|
|
func shouldMeterRequest(req *http.Request) bool {
|
2021-04-27 13:52:12 -04:00
|
|
|
return !(guessIsHealthCheckReq(req) || guessIsMetricsReq(req))
|
2020-02-11 10:38:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// MetricsTransport is a custom wrapper around Transport to track metrics
|
|
|
|
type MetricsTransport struct {
|
|
|
|
Transport *http.Transport
|
2021-01-18 23:35:38 -05:00
|
|
|
Metrics *BackendMetrics
|
2020-02-11 10:38:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// RoundTrip implements the RoundTrip method for MetricsTransport
|
|
|
|
func (m MetricsTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
|
|
metered := shouldMeterRequest(r)
|
2020-04-01 15:52:31 -04:00
|
|
|
if metered && (r.Method == http.MethodPost || r.Method == http.MethodPut) {
|
2020-02-11 10:38:01 -05:00
|
|
|
m.Metrics.IncRequests(r.Method)
|
2020-02-11 23:45:00 -05:00
|
|
|
if r.ContentLength > 0 {
|
|
|
|
m.Metrics.IncBytesSent(uint64(r.ContentLength))
|
|
|
|
}
|
2020-02-11 10:38:01 -05:00
|
|
|
}
|
|
|
|
// Make the request to the server.
|
|
|
|
resp, err := m.Transport.RoundTrip(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if metered && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
2020-04-01 15:52:31 -04:00
|
|
|
m.Metrics.IncRequests(r.Method)
|
|
|
|
if resp.ContentLength > 0 {
|
2020-02-11 23:45:00 -05:00
|
|
|
m.Metrics.IncBytesReceived(uint64(resp.ContentLength))
|
|
|
|
}
|
2020-02-11 10:38:01 -05:00
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|