github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/backend/aws.go (about)

     1  //go:build aws
     2  
     3  // Package backend contains implementation of various backend providers.
     4  /*
     5   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     6   */
     7  package backend
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"os"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"time"
    20  
    21  	aiss3 "github.com/NVIDIA/aistore/ais/s3"
    22  	"github.com/NVIDIA/aistore/api/apc"
    23  	"github.com/NVIDIA/aistore/api/env"
    24  	"github.com/NVIDIA/aistore/cmn"
    25  	"github.com/NVIDIA/aistore/cmn/cos"
    26  	"github.com/NVIDIA/aistore/cmn/debug"
    27  	"github.com/NVIDIA/aistore/cmn/feat"
    28  	"github.com/NVIDIA/aistore/cmn/nlog"
    29  	"github.com/NVIDIA/aistore/core"
    30  	"github.com/NVIDIA/aistore/core/meta"
    31  	"github.com/NVIDIA/aistore/memsys"
    32  	"github.com/aws/aws-sdk-go-v2/aws"
    33  	awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
    34  	"github.com/aws/aws-sdk-go-v2/config"
    35  	s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
    36  	"github.com/aws/aws-sdk-go-v2/service/s3"
    37  	"github.com/aws/aws-sdk-go-v2/service/s3/types"
    38  	"github.com/aws/smithy-go"
    39  )
    40  
    41  type (
    42  	s3bp struct {
    43  		t  core.TargetPut
    44  		mm *memsys.MMSA
    45  		base
    46  	}
    47  	sessConf struct {
    48  		bck    *cmn.Bck
    49  		region string
    50  	}
    51  )
    52  
    53  var (
    54  	// map[string]*s3.Client, with one s3.Client a.k.a. "svc"
    55  	// per (profile, region, endpoint) triplet
    56  	clients sync.Map
    57  
    58  	s3Endpoint string
    59  	awsProfile string
    60  )
    61  
    62  // interface guard
    63  var _ core.Backend = (*s3bp)(nil)
    64  
    65  // environment variables => static defaults that can still be overridden via bck.Props.Extra.AWS
    66  // in addition to these two (below), default bucket region = env.AwsDefaultRegion()
    67  func NewAWS(t core.TargetPut) (core.Backend, error) {
    68  	s3Endpoint = os.Getenv(env.AWS.Endpoint)
    69  	awsProfile = os.Getenv(env.AWS.Profile)
    70  	return &s3bp{
    71  		t:    t,
    72  		mm:   t.PageMM(),
    73  		base: base{apc.AWS},
    74  	}, nil
    75  }
    76  
    77  // as core.Backend --------------------------------------------------------------
    78  
    79  //
    80  // HEAD BUCKET
    81  //
    82  
    83  const gotBucketLocation = "got_bucket_location"
    84  
    85  func (*s3bp) HeadBucket(_ context.Context, bck *meta.Bck) (bckProps cos.StrKVs, ecode int, _ error) {
    86  	var (
    87  		cloudBck = bck.RemoteBck()
    88  		sessConf = sessConf{bck: cloudBck}
    89  	)
    90  	svc, err := sessConf.s3client("")
    91  	if err != nil {
    92  		return nil, 0, err
    93  	}
    94  	if cmn.Rom.FastV(5, cos.SmoduleBackend) {
    95  		nlog.Infoln("[head_bucket]", cloudBck.Name)
    96  	}
    97  	if sessConf.region == "" {
    98  		var region string
    99  		if region, err = getBucketLocation(svc, cloudBck.Name); err != nil {
   100  			ecode, err = awsErrorToAISError(err, cloudBck, "")
   101  			return nil, ecode, err
   102  		}
   103  		if cmn.Rom.FastV(4, cos.SmoduleBackend) {
   104  			nlog.Infoln("get-bucket-location", cloudBck.Name, "region", region)
   105  		}
   106  		svc, err = sessConf.s3client(gotBucketLocation)
   107  		debug.AssertNoErr(err)
   108  	}
   109  
   110  	// NOTE: return a few assorted fields, specifically to fill-in vendor-specific `cmn.ExtraProps`
   111  	bckProps = make(cos.StrKVs, 4)
   112  	bckProps[apc.HdrBackendProvider] = apc.AWS
   113  	bckProps[apc.HdrS3Region] = sessConf.region
   114  	bckProps[apc.HdrS3Endpoint] = ""
   115  	if bck.Props != nil {
   116  		bckProps[apc.HdrS3Endpoint] = bck.Props.Extra.AWS.Endpoint
   117  	}
   118  	versioned, errV := getBucketVersioning(svc, cloudBck)
   119  	if errV != nil {
   120  		ecode, err = awsErrorToAISError(errV, cloudBck, "")
   121  		return nil, ecode, err
   122  	}
   123  	bckProps[apc.HdrBucketVerEnabled] = strconv.FormatBool(versioned)
   124  	return bckProps, 0, nil
   125  }
   126  
   127  //
   128  // LIST OBJECTS via INVENTORY
   129  //
   130  
   131  // when successful, returns w/ rlock held and inventory's (lom, lmfh) in the context;
   132  // otherwise, always unlocks and frees
   133  func (s3bp *s3bp) GetBucketInv(bck *meta.Bck, ctx *core.LsoInvCtx) (int, error) {
   134  	debug.Assert(ctx != nil && ctx.Lom == nil)
   135  	var (
   136  		cloudBck = bck.RemoteBck()
   137  		sessConf = sessConf{bck: cloudBck}
   138  	)
   139  	svc, err := sessConf.s3client("[get_bucket_inv]")
   140  	if err != nil {
   141  		return 0, err
   142  	}
   143  
   144  	// one bucket, one inventory, one statically defined name
   145  	prefix, objName := aiss3.InvPrefObjname(bck.Bucket(), ctx.Name, ctx.ID)
   146  	lom := core.AllocLOM(objName)
   147  	if err = lom.InitBck(bck.Bucket()); err != nil {
   148  		core.FreeLOM(lom)
   149  		return 0, err
   150  	}
   151  	if !lom.TryLock(false) {
   152  		err = cmn.NewErrBusy(invTag, lom.Cname(), "likely getting updated")
   153  		core.FreeLOM(lom)
   154  		return 0, err
   155  	}
   156  
   157  	lsV2resp, csv, manifest, ecode, err := s3bp.initInventory(cloudBck, svc, ctx, prefix)
   158  	if err != nil {
   159  		lom.Unlock(false)
   160  		core.FreeLOM(lom)
   161  		return ecode, err
   162  	}
   163  	ctx.Lom = lom
   164  	mtime, usable := checkInvLom(csv.mtime, ctx)
   165  	if usable {
   166  		if ctx.Lmfh, err = ctx.Lom.OpenFile(); err != nil {
   167  			lom.Unlock(false)
   168  			core.FreeLOM(lom)
   169  			ctx.Lom = nil
   170  			return 0, _errInv("usable-inv-open", err)
   171  		}
   172  
   173  		return 0, nil // w/ rlock
   174  	}
   175  
   176  	// rlock -> wlock
   177  
   178  	lom.Unlock(false)
   179  	err = cmn.NewErrBusy(invTag, lom.Cname(), "timed out waiting to acquire write access") // prelim
   180  	sleep, total := time.Second, invBusyTimeout
   181  	for total >= 0 {
   182  		if lom.TryLock(true) {
   183  			err = nil
   184  			break
   185  		}
   186  		time.Sleep(sleep)
   187  		total -= sleep
   188  	}
   189  	if err != nil {
   190  		core.FreeLOM(lom)
   191  		ctx.Lom = nil
   192  		return 0, err // busy
   193  	}
   194  
   195  	// acquired wlock: check for write/write race
   196  
   197  	finfo, err := os.Stat(ctx.Lom.FQN)
   198  	if err == nil {
   199  		newMtime := finfo.ModTime()
   200  		if newMtime.Sub(mtime) > time.Hour {
   201  			// updated by smbd else
   202  			// reload the lom and return
   203  			ctx.Lom.Uncache()
   204  			_, usable = checkInvLom(newMtime, ctx)
   205  			debug.Assert(usable)
   206  
   207  			// wlock --> rlock must succeed
   208  			lom.Unlock(true)
   209  			lom.Lock(false)
   210  
   211  			if ctx.Lmfh, err = ctx.Lom.OpenFile(); err != nil {
   212  				lom.Unlock(false)
   213  				core.FreeLOM(lom)
   214  				ctx.Lom = nil
   215  				return 0, _errInv("reload-inv-open", err)
   216  			}
   217  			return 0, nil // ok
   218  		}
   219  	}
   220  
   221  	// still under wlock: cleanup old, read and write as ctx.Lom
   222  
   223  	cleanupOldInventory(cloudBck, svc, lsV2resp, csv, manifest)
   224  
   225  	err = s3bp.getInventory(cloudBck, ctx, csv)
   226  
   227  	// wlock --> rlock
   228  
   229  	lom.Unlock(true)
   230  
   231  	if err != nil {
   232  		core.FreeLOM(lom)
   233  		ctx.Lom = nil
   234  		return 0, err
   235  	}
   236  
   237  	lom.Lock(false) // must succeed
   238  	if ctx.Lmfh, err = ctx.Lom.OpenFile(); err != nil {
   239  		lom.Unlock(false)
   240  		core.FreeLOM(lom)
   241  		ctx.Lom = nil
   242  		return 0, _errInv("get-inv-open", err)
   243  	}
   244  
   245  	return 0, nil // ok
   246  }
   247  
   248  // using local(ized) .csv
   249  func (s3bp *s3bp) ListObjectsInv(bck *meta.Bck, msg *apc.LsoMsg, lst *cmn.LsoRes, ctx *core.LsoInvCtx) (err error) {
   250  	debug.Assert(ctx.Lom != nil && ctx.Lmfh != nil, ctx.Lom, " ", ctx.Lmfh)
   251  
   252  	cloudBck := bck.RemoteBck()
   253  
   254  	if ctx.SGL == nil {
   255  		if ctx.EOF {
   256  			debug.Assert(false) // (unlikely)
   257  			goto none
   258  		}
   259  		ctx.SGL = s3bp.mm.NewSGL(invPageSGL, memsys.DefaultBuf2Size)
   260  	} else if l := ctx.SGL.Len(); l > 0 && l < invSwapSGL && !ctx.EOF {
   261  		// swap SGLs
   262  		sgl := s3bp.mm.NewSGL(invPageSGL, memsys.DefaultBuf2Size)
   263  		written, err := io.Copy(sgl, ctx.SGL) // buffering not needed - gets executed via sgl WriteTo()
   264  		debug.AssertNoErr(err)
   265  		debug.Assert(written == l && sgl.Len() == l, written, " vs ", l, " vs ", sgl.Len())
   266  		ctx.SGL.Free()
   267  		ctx.SGL = sgl
   268  	}
   269  	err = s3bp.listInventory(cloudBck, ctx, msg, lst)
   270  
   271  	if err == nil || err == io.EOF {
   272  		return nil
   273  	}
   274  none:
   275  	lst.Entries = lst.Entries[:0]
   276  	return err
   277  }
   278  
   279  //
   280  // LIST OBJECTS
   281  //
   282  
   283  // NOTE: obtaining versioning info is extremely slow - to avoid timeouts, imposing a hard limit on the page size
   284  const versionedPageSize = 20
   285  
   286  func (*s3bp) ListObjects(bck *meta.Bck, msg *apc.LsoMsg, lst *cmn.LsoRes) (ecode int, _ error) {
   287  	var (
   288  		h          = cmn.BackendHelpers.Amazon
   289  		cloudBck   = bck.RemoteBck()
   290  		sessConf   = sessConf{bck: cloudBck}
   291  		versioning bool
   292  	)
   293  	svc, err := sessConf.s3client("[list_objects]")
   294  	if err != nil {
   295  		return 0, err
   296  	}
   297  	params := &s3.ListObjectsV2Input{Bucket: aws.String(cloudBck.Name)}
   298  	if prefix := msg.Prefix; prefix != "" {
   299  		if msg.IsFlagSet(apc.LsNoRecursion) {
   300  			// NOTE: important to indicate subdirectory with trailing '/'
   301  			if cos.IsLastB(prefix, '/') {
   302  				params.Delimiter = aws.String("/")
   303  			}
   304  		}
   305  		params.Prefix = aws.String(prefix)
   306  	}
   307  	if msg.ContinuationToken != "" {
   308  		params.ContinuationToken = aws.String(msg.ContinuationToken)
   309  	}
   310  
   311  	versioning = bck.Props != nil && bck.Props.Versioning.Enabled && msg.WantProp(apc.GetPropsVersion)
   312  	msg.PageSize = calcPageSize(msg.PageSize, bck.MaxPageSize())
   313  	if versioning {
   314  		msg.PageSize = min(versionedPageSize, msg.PageSize)
   315  	}
   316  	params.MaxKeys = aws.Int32(int32(msg.PageSize))
   317  
   318  	resp, err := svc.ListObjectsV2(context.Background(), params)
   319  	if err != nil {
   320  		if cmn.Rom.FastV(4, cos.SmoduleBackend) {
   321  			nlog.Infoln("list_objects", cloudBck.Name, err)
   322  		}
   323  		ecode, err = awsErrorToAISError(err, cloudBck, "")
   324  		return ecode, err
   325  	}
   326  
   327  	var (
   328  		custom     cos.StrKVs
   329  		l          = len(resp.Contents)
   330  		wantCustom = msg.WantProp(apc.GetPropsCustom)
   331  	)
   332  	for i := len(lst.Entries); i < l; i++ {
   333  		lst.Entries = append(lst.Entries, &cmn.LsoEnt{}) // add missing empty
   334  	}
   335  	if wantCustom {
   336  		custom = make(cos.StrKVs, 2) // reuse
   337  	}
   338  	for i, obj := range resp.Contents {
   339  		entry := lst.Entries[i]
   340  		entry.Name = *obj.Key
   341  		entry.Size = *obj.Size
   342  		if msg.IsFlagSet(apc.LsNameOnly) || msg.IsFlagSet(apc.LsNameSize) {
   343  			continue
   344  		}
   345  		if v, ok := h.EncodeCksum(obj.ETag); ok {
   346  			entry.Checksum = v
   347  		}
   348  		if wantCustom {
   349  			custom[cmn.ETag] = entry.Checksum
   350  			mtime := *(obj.LastModified)
   351  			custom[cmn.LastModified] = fmtTime(mtime)
   352  			entry.Custom = cmn.CustomMD2S(custom)
   353  		}
   354  	}
   355  	lst.Entries = lst.Entries[:l]
   356  
   357  	if *resp.IsTruncated {
   358  		lst.ContinuationToken = *resp.NextContinuationToken
   359  	}
   360  
   361  	if len(lst.Entries) == 0 || !versioning {
   362  		if cmn.Rom.FastV(4, cos.SmoduleBackend) {
   363  			nlog.Infoln("[list_objects]", cloudBck.Name, len(lst.Entries))
   364  		}
   365  		return 0, nil
   366  	}
   367  
   368  	// [slow path] for each already listed object:
   369  	// - set the `ListObjectVersionsInput.Prefix` to the object's full name
   370  	// - get the versions and lookup the latest one
   371  	var (
   372  		verParams = &s3.ListObjectVersionsInput{Bucket: aws.String(cloudBck.Name)}
   373  		num       int
   374  	)
   375  	for _, entry := range lst.Entries {
   376  		verParams.Prefix = aws.String(entry.Name)
   377  		verResp, err := svc.ListObjectVersions(context.Background(), verParams)
   378  		if err != nil {
   379  			return awsErrorToAISError(err, cloudBck, "")
   380  		}
   381  		for _, vers := range verResp.Versions {
   382  			if latest := *(vers.IsLatest); !latest {
   383  				continue
   384  			}
   385  			if key := *(vers.Key); key == entry.Name {
   386  				v, ok := h.EncodeVersion(vers.VersionId)
   387  				debug.Assert(ok, entry.Name+": "+*(vers.VersionId))
   388  				entry.Version = v
   389  				num++
   390  			}
   391  		}
   392  	}
   393  	if cmn.Rom.FastV(4, cos.SmoduleBackend) {
   394  		nlog.Infoln("[list_objects]", cloudBck.Name, len(lst.Entries), num)
   395  	}
   396  	return 0, nil
   397  }
   398  
   399  //
   400  // LIST BUCKETS
   401  //
   402  
   403  func (*s3bp) ListBuckets(cmn.QueryBcks) (bcks cmn.Bcks, ecode int, _ error) {
   404  	var (
   405  		sessConf sessConf
   406  		result   *s3.ListBucketsOutput
   407  	)
   408  	svc, err := sessConf.s3client("")
   409  	if err != nil {
   410  		ecode, err = awsErrorToAISError(err, &cmn.Bck{Provider: apc.AWS}, "")
   411  		return nil, ecode, err
   412  	}
   413  	result, err = svc.ListBuckets(context.Background(), &s3.ListBucketsInput{})
   414  	if err != nil {
   415  		ecode, err = awsErrorToAISError(err, &cmn.Bck{Provider: apc.AWS}, "")
   416  		return nil, ecode, err
   417  	}
   418  
   419  	bcks = make(cmn.Bcks, len(result.Buckets))
   420  	for idx, bck := range result.Buckets {
   421  		if cmn.Rom.FastV(4, cos.SmoduleBackend) {
   422  			nlog.Infoln("[bucket_names]", aws.ToString(bck.Name), "created", *bck.CreationDate)
   423  		}
   424  		bcks[idx] = cmn.Bck{
   425  			Name:     aws.ToString(bck.Name),
   426  			Provider: apc.AWS,
   427  		}
   428  	}
   429  	return bcks, 0, nil
   430  }
   431  
   432  //
   433  // HEAD OBJECT
   434  //
   435  
   436  func (*s3bp) HeadObj(_ context.Context, lom *core.LOM, oreq *http.Request) (oa *cmn.ObjAttrs, ecode int, err error) {
   437  	var (
   438  		svc        *s3.Client
   439  		headOutput *s3.HeadObjectOutput
   440  		h          = cmn.BackendHelpers.Amazon
   441  		cloudBck   = lom.Bck().RemoteBck()
   442  		sessConf   = sessConf{bck: cloudBck}
   443  	)
   444  
   445  	if lom.IsFeatureSet(feat.S3PresignedRequest) && oreq != nil {
   446  		q := oreq.URL.Query() // TODO: optimize-out
   447  		pts := aiss3.NewPresignedReq(oreq, lom, nil, q)
   448  		resp, err := pts.Do(core.T.DataClient())
   449  		if err != nil {
   450  			return nil, resp.StatusCode, err
   451  		}
   452  		if resp != nil {
   453  			oa = resp.ObjAttrs()
   454  			goto exit
   455  		}
   456  	}
   457  
   458  	svc, err = sessConf.s3client("[head_object]")
   459  	if err != nil {
   460  		return
   461  	}
   462  	headOutput, err = svc.HeadObject(context.Background(), &s3.HeadObjectInput{
   463  		Bucket: aws.String(cloudBck.Name),
   464  		Key:    aws.String(lom.ObjName),
   465  	})
   466  	if err != nil {
   467  		ecode, err = awsErrorToAISError(err, cloudBck, lom.ObjName)
   468  		return
   469  	}
   470  	oa = &cmn.ObjAttrs{}
   471  	oa.CustomMD = make(cos.StrKVs, 6)
   472  	oa.SetCustomKey(cmn.SourceObjMD, apc.AWS)
   473  	oa.Size = *headOutput.ContentLength
   474  	if v, ok := h.EncodeVersion(headOutput.VersionId); ok {
   475  		lom.SetCustomKey(cmn.VersionObjMD, v)
   476  		oa.Ver = v
   477  	}
   478  	if v, ok := h.EncodeCksum(headOutput.ETag); ok {
   479  		oa.SetCustomKey(cmn.ETag, v)
   480  		// assuming SSE-S3 or plaintext encryption
   481  		// from https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html:
   482  		// - "The entity tag is a hash of the object. The ETag reflects changes only
   483  		//    to the contents of an object, not its metadata."
   484  		// - "The ETag may or may not be an MD5 digest of the object data. Whether or
   485  		//    not it is depends on how the object was created and how it is encrypted..."
   486  		if !cmn.IsS3MultipartEtag(v) {
   487  			oa.SetCustomKey(cmn.MD5ObjMD, v)
   488  		}
   489  	}
   490  
   491  	// AIS custom (see also: PutObject, GetObjReader)
   492  	if cksumType, ok := headOutput.Metadata[cos.S3MetadataChecksumType]; ok {
   493  		if cksumValue, ok := headOutput.Metadata[cos.S3MetadataChecksumVal]; ok {
   494  			oa.SetCksum(cksumType, cksumValue)
   495  		}
   496  	}
   497  
   498  	// unlike other custom attrs, "Content-Type" is not getting stored w/ LOM
   499  	// - only shown via list-objects and HEAD when not present
   500  	if v := headOutput.ContentType; v != nil {
   501  		oa.SetCustomKey(cos.HdrContentType, *v)
   502  	}
   503  	if v := headOutput.LastModified; v != nil {
   504  		mtime := *(headOutput.LastModified)
   505  		if oa.Atime == 0 {
   506  			oa.Atime = mtime.UnixNano()
   507  		}
   508  		oa.SetCustomKey(cmn.LastModified, fmtTime(mtime))
   509  	}
   510  
   511  exit:
   512  	if cmn.Rom.FastV(5, cos.SmoduleBackend) {
   513  		nlog.Infoln("[head_object]", cloudBck.Cname(lom.ObjName))
   514  	}
   515  	return
   516  }
   517  
   518  //
   519  // GET OBJECT
   520  //
   521  
   522  func (s3bp *s3bp) GetObj(ctx context.Context, lom *core.LOM, owt cmn.OWT, oreq *http.Request) (int, error) {
   523  	var res core.GetReaderResult
   524  
   525  	if lom.IsFeatureSet(feat.S3PresignedRequest) && oreq != nil {
   526  		q := oreq.URL.Query() // TODO: optimize-out
   527  		pts := aiss3.NewPresignedReq(oreq, lom, nil, q)
   528  		resp, err := pts.DoReader(core.T.DataClient())
   529  		if err != nil {
   530  			res = core.GetReaderResult{Err: err, ErrCode: resp.StatusCode}
   531  			goto finalize
   532  		}
   533  		if resp != nil {
   534  			res = core.GetReaderResult{
   535  				R:       resp.BodyR,
   536  				Size:    resp.Size,
   537  				ErrCode: resp.StatusCode,
   538  			}
   539  			goto finalize
   540  		}
   541  	}
   542  
   543  	res = s3bp.GetObjReader(ctx, lom, 0, 0)
   544  
   545  finalize:
   546  	if res.Err != nil {
   547  		return res.ErrCode, res.Err
   548  	}
   549  	params := allocPutParams(res, owt)
   550  	err := s3bp.t.PutObject(lom, params)
   551  	core.FreePutParams(params)
   552  	if cmn.Rom.FastV(5, cos.SmoduleBackend) {
   553  		nlog.Infoln("[get_object]", lom.String(), err)
   554  	}
   555  	return 0, err
   556  }
   557  
   558  func (*s3bp) GetObjReader(ctx context.Context, lom *core.LOM, offset, length int64) (res core.GetReaderResult) {
   559  	var (
   560  		obj      *s3.GetObjectOutput
   561  		cloudBck = lom.Bck().RemoteBck()
   562  		sessConf = sessConf{bck: cloudBck}
   563  		input    = s3.GetObjectInput{
   564  			Bucket: aws.String(cloudBck.Name),
   565  			Key:    aws.String(lom.ObjName),
   566  		}
   567  	)
   568  	svc, err := sessConf.s3client("[get_obj_reader]")
   569  	if err != nil {
   570  		res.Err = err
   571  		return
   572  	}
   573  	if length > 0 {
   574  		rng := cmn.MakeRangeHdr(offset, length)
   575  		input.Range = aws.String(rng)
   576  		obj, err = svc.GetObject(ctx, &input)
   577  		if err != nil {
   578  			res.ErrCode, res.Err = awsErrorToAISError(err, cloudBck, lom.ObjName)
   579  			if res.ErrCode == http.StatusRequestedRangeNotSatisfiable {
   580  				res.Err = cmn.NewErrRangeNotSatisfiable(res.Err, nil, 0)
   581  			}
   582  			return res
   583  		}
   584  	} else {
   585  		obj, err = svc.GetObject(ctx, &input)
   586  		if err != nil {
   587  			res.ErrCode, res.Err = awsErrorToAISError(err, cloudBck, lom.ObjName)
   588  			return res
   589  		}
   590  		// custom metadata
   591  		lom.SetCustomKey(cmn.SourceObjMD, apc.AWS)
   592  
   593  		res.ExpCksum = _getCustom(lom, obj)
   594  
   595  		md := obj.Metadata
   596  		if cksumType, ok := md[cos.S3MetadataChecksumType]; ok {
   597  			if cksumValue, ok := md[cos.S3MetadataChecksumVal]; ok {
   598  				cksum := cos.NewCksum(cksumType, cksumValue)
   599  				lom.SetCksum(cksum)
   600  				res.ExpCksum = cksum // precedence over md5 (<= ETag)
   601  			}
   602  		}
   603  	}
   604  
   605  	res.R = obj.Body
   606  	res.Size = *obj.ContentLength
   607  	return res
   608  }
   609  
   610  func _getCustom(lom *core.LOM, obj *s3.GetObjectOutput) (md5 *cos.Cksum) {
   611  	h := cmn.BackendHelpers.Amazon
   612  	if v, ok := h.EncodeVersion(obj.VersionId); ok {
   613  		lom.SetVersion(v)
   614  		lom.SetCustomKey(cmn.VersionObjMD, v)
   615  	}
   616  	// see ETag/MD5 NOTE above
   617  	if v, ok := h.EncodeCksum(obj.ETag); ok {
   618  		lom.SetCustomKey(cmn.ETag, v)
   619  		if !cmn.IsS3MultipartEtag(v) {
   620  			md5 = cos.NewCksum(cos.ChecksumMD5, v)
   621  			lom.SetCustomKey(cmn.MD5ObjMD, v)
   622  		}
   623  	}
   624  	mtime := *(obj.LastModified)
   625  	lom.SetCustomKey(cmn.LastModified, fmtTime(mtime))
   626  	return
   627  }
   628  
   629  //
   630  // PUT OBJECT
   631  //
   632  
   633  func (*s3bp) PutObj(r io.ReadCloser, lom *core.LOM, oreq *http.Request) (ecode int, err error) {
   634  	var (
   635  		svc                   *s3.Client
   636  		uploader              *s3manager.Uploader
   637  		uploadOutput          *s3manager.UploadOutput
   638  		h                     = cmn.BackendHelpers.Amazon
   639  		cksumType, cksumValue = lom.Checksum().Get()
   640  		cloudBck              = lom.Bck().RemoteBck()
   641  		sessConf              = sessConf{bck: cloudBck}
   642  		md                    = make(map[string]string, 2)
   643  	)
   644  	if lom.IsFeatureSet(feat.S3PresignedRequest) && oreq != nil {
   645  		q := oreq.URL.Query() // TODO: optimize-out
   646  		pts := aiss3.NewPresignedReq(oreq, lom, r, q)
   647  		resp, err := pts.Do(core.T.DataClient())
   648  		if err != nil {
   649  			return resp.StatusCode, err
   650  		}
   651  		if resp != nil {
   652  			uploadOutput = &s3manager.UploadOutput{
   653  				ETag: aws.String(resp.Header.Get(cos.HdrETag)),
   654  			}
   655  			goto exit
   656  		}
   657  	}
   658  
   659  	svc, err = sessConf.s3client("[put_object]")
   660  	if err != nil {
   661  		return
   662  	}
   663  
   664  	md[cos.S3MetadataChecksumType] = cksumType
   665  	md[cos.S3MetadataChecksumVal] = cksumValue
   666  
   667  	uploader = s3manager.NewUploader(svc)
   668  	uploadOutput, err = uploader.Upload(context.Background(), &s3.PutObjectInput{
   669  		Bucket:   aws.String(cloudBck.Name),
   670  		Key:      aws.String(lom.ObjName),
   671  		Body:     r,
   672  		Metadata: md,
   673  	})
   674  	if err != nil {
   675  		ecode, err = awsErrorToAISError(err, cloudBck, lom.ObjName)
   676  		cos.Close(r)
   677  		return
   678  	}
   679  
   680  exit:
   681  	// compare with setCustomS3() above
   682  	if v, ok := h.EncodeVersion(uploadOutput.VersionID); ok {
   683  		lom.SetCustomKey(cmn.VersionObjMD, v)
   684  		lom.SetVersion(v)
   685  	}
   686  	if v, ok := h.EncodeCksum(uploadOutput.ETag); ok {
   687  		lom.SetCustomKey(cmn.ETag, v)
   688  		// see ETag/MD5 NOTE above
   689  		if !cmn.IsS3MultipartEtag(v) {
   690  			lom.SetCustomKey(cmn.MD5ObjMD, v)
   691  		}
   692  	}
   693  	if cmn.Rom.FastV(5, cos.SmoduleBackend) {
   694  		nlog.Infoln("[put_object]", lom.String())
   695  	}
   696  	cos.Close(r)
   697  	return
   698  }
   699  
   700  //
   701  // DELETE OBJECT
   702  //
   703  
   704  func (*s3bp) DeleteObj(lom *core.LOM) (ecode int, err error) {
   705  	var (
   706  		svc      *s3.Client
   707  		cloudBck = lom.Bck().RemoteBck()
   708  		sessConf = sessConf{bck: cloudBck}
   709  	)
   710  	svc, err = sessConf.s3client("[delete_object]")
   711  	if err != nil {
   712  		return
   713  	}
   714  	_, err = svc.DeleteObject(context.Background(), &s3.DeleteObjectInput{
   715  		Bucket: aws.String(cloudBck.Name),
   716  		Key:    aws.String(lom.ObjName),
   717  	})
   718  	if err != nil {
   719  		ecode, err = awsErrorToAISError(err, cloudBck, lom.ObjName)
   720  		return
   721  	}
   722  	if cmn.Rom.FastV(5, cos.SmoduleBackend) {
   723  		nlog.Infoln("[delete_object]", lom.String())
   724  	}
   725  	return
   726  }
   727  
   728  //
   729  // static helpers
   730  //
   731  
   732  // newClient creates new S3 client on a per-region basis or, more precisely,
   733  // per (region, endpoint) pair - and note that s3 endpoint is per-bucket configurable.
   734  // If the client already exists newClient simply returns it.
   735  // From S3 SDK:
   736  // "S3 methods are safe to use concurrently. It is not safe to modify mutate
   737  // any of the struct's properties though."
   738  func (sessConf *sessConf) s3client(tag string) (*s3.Client, error) {
   739  	var (
   740  		endpoint = s3Endpoint
   741  		profile  = awsProfile
   742  	)
   743  	if sessConf.bck != nil && sessConf.bck.Props != nil {
   744  		if sessConf.region == "" {
   745  			sessConf.region = sessConf.bck.Props.Extra.AWS.CloudRegion
   746  		}
   747  		if sessConf.bck.Props.Extra.AWS.Endpoint != "" {
   748  			endpoint = sessConf.bck.Props.Extra.AWS.Endpoint
   749  		}
   750  		if sessConf.bck.Props.Extra.AWS.Profile != "" {
   751  			profile = sessConf.bck.Props.Extra.AWS.Profile
   752  		}
   753  	}
   754  
   755  	cid := _cid(profile, sessConf.region, endpoint)
   756  	asvc, loaded := clients.Load(cid)
   757  	if loaded {
   758  		svc, ok := asvc.(*s3.Client)
   759  		debug.Assert(ok)
   760  		return svc, nil
   761  	}
   762  
   763  	// slow path
   764  	cfg, err := loadConfig(endpoint, profile)
   765  	if err != nil {
   766  		return nil, err
   767  	}
   768  
   769  	svc := s3.NewFromConfig(cfg, sessConf.options)
   770  
   771  	// NOTE:
   772  	// - gotBucketLocation special case
   773  	// - otherwise, not caching s3 client for an unknown or missing region
   774  	if sessConf.region == "" && tag != gotBucketLocation {
   775  		if tag != "" && cmn.Rom.FastV(4, cos.SmoduleBackend) {
   776  			nlog.Warningln(tag, "no region for bucket", sessConf.bck.Cname(""))
   777  		}
   778  		return svc, nil
   779  	}
   780  
   781  	// cache (without recomputing _cid and possibly an empty region)
   782  	if cmn.Rom.FastV(4, cos.SmoduleBackend) {
   783  		nlog.Infoln("add s3client for tuple (profile, region, endpoint):", cid)
   784  	}
   785  	clients.Store(cid, svc) // race or no race, no particular reason to do LoadOrStore
   786  	return svc, nil
   787  }
   788  
   789  func (sessConf *sessConf) options(options *s3.Options) {
   790  	if sessConf.region != "" {
   791  		options.Region = sessConf.region
   792  	} else {
   793  		sessConf.region = options.Region
   794  	}
   795  	if bck := sessConf.bck; bck != nil {
   796  		if bck.Props != nil {
   797  			options.UsePathStyle = bck.Props.Features.IsSet(feat.S3UsePathStyle)
   798  		} else {
   799  			options.UsePathStyle = cmn.Rom.Features().IsSet(feat.S3UsePathStyle)
   800  		}
   801  	}
   802  }
   803  
   804  func _cid(profile, region, endpoint string) string {
   805  	sb := &strings.Builder{}
   806  	if profile != "" {
   807  		sb.WriteString(profile)
   808  	}
   809  	sb.WriteByte('#')
   810  	if region != "" {
   811  		sb.WriteString(region)
   812  	}
   813  	sb.WriteByte('#')
   814  	if endpoint != "" {
   815  		sb.WriteString(endpoint)
   816  	}
   817  	return sb.String()
   818  }
   819  
   820  // loadConfig create config using default creds from ~/.aws/credentials and environment variables.
   821  func loadConfig(endpoint, profile string) (aws.Config, error) {
   822  	// NOTE: The AWS SDK for Go v2, uses lower case header maps by default.
   823  	cfg, err := config.LoadDefaultConfig(
   824  		context.Background(),
   825  		config.WithHTTPClient(cmn.NewClient(cmn.TransportArgs{})),
   826  		config.WithSharedConfigProfile(profile),
   827  	)
   828  	if err != nil {
   829  		return cfg, err
   830  	}
   831  	if endpoint != "" {
   832  		cfg.BaseEndpoint = aws.String(endpoint)
   833  	}
   834  	return cfg, nil
   835  }
   836  
   837  func getBucketVersioning(svc *s3.Client, bck *cmn.Bck) (enabled bool, errV error) {
   838  	input := &s3.GetBucketVersioningInput{Bucket: aws.String(bck.Name)}
   839  	result, err := svc.GetBucketVersioning(context.Background(), input)
   840  	if err != nil {
   841  		return false, err
   842  	}
   843  	enabled = result.Status == types.BucketVersioningStatusEnabled
   844  	return
   845  }
   846  
   847  func getBucketLocation(svc *s3.Client, bckName string) (region string, err error) {
   848  	resp, err := svc.GetBucketLocation(context.Background(), &s3.GetBucketLocationInput{
   849  		Bucket: aws.String(bckName),
   850  	})
   851  	if err != nil {
   852  		return
   853  	}
   854  	region = string(resp.LocationConstraint)
   855  	if region == "" {
   856  		region = env.AwsDefaultRegion() // env "AWS_REGION" or "us-east-1" - in that order
   857  	}
   858  	return
   859  }
   860  
   861  // For reference see https://github.com/aws/aws-sdk-go-v2/issues/1110#issuecomment-1054643716.
   862  func awsErrorToAISError(awsError error, bck *cmn.Bck, objName string) (int, error) {
   863  	if cmn.Rom.FastV(5, cos.SmoduleBackend) {
   864  		nlog.InfoDepth(1, "begin "+aiss3.ErrPrefix+" =========================")
   865  		nlog.InfoDepth(1, awsError)
   866  		nlog.InfoDepth(1, "end "+aiss3.ErrPrefix+" ===========================")
   867  	}
   868  
   869  	var reqErr smithy.APIError
   870  	if !errors.As(awsError, &reqErr) {
   871  		return http.StatusInternalServerError, _awsErr(awsError, "")
   872  	}
   873  
   874  	switch reqErr.(type) {
   875  	case *types.NoSuchBucket:
   876  		return http.StatusNotFound, cmn.NewErrRemoteBckNotFound(bck)
   877  	case *types.NoSuchKey:
   878  		e := fmt.Errorf("%s[%s: %s]", aiss3.ErrPrefix, reqErr.ErrorCode(), bck.Cname(objName))
   879  		return http.StatusNotFound, e
   880  	default:
   881  		var (
   882  			rspErr *awshttp.ResponseError
   883  			code   = reqErr.ErrorCode()
   884  		)
   885  		if errors.As(awsError, &rspErr) {
   886  			return rspErr.HTTPStatusCode(), _awsErr(awsError, code)
   887  		}
   888  
   889  		return http.StatusBadRequest, _awsErr(awsError, code)
   890  	}
   891  }
   892  
   893  // Strip original AWS error to its essentials: type code and error message
   894  // See also:
   895  // * ais/s3/err.go WriteErr() that (NOTE) relies on the formatting below
   896  // * aws-sdk-go/aws/awserr/types.go
   897  func _awsErr(awsError error, code string) error {
   898  	var (
   899  		msg        = awsError.Error()
   900  		origErrMsg = awsError.Error()
   901  	)
   902  	// Strip extra information
   903  	if idx := strings.Index(msg, "\n\t"); idx > 0 {
   904  		msg = msg[:idx]
   905  	}
   906  	// ...but preserve original error information.
   907  	if idx := strings.Index(origErrMsg, "\ncaused"); idx > 0 {
   908  		// `idx+1` because we want to remove `\n`.
   909  		msg += " (" + origErrMsg[idx+1:] + ")"
   910  	}
   911  	if code != "" {
   912  		if i := strings.Index(msg, code+": "); i > 0 {
   913  			msg = msg[i:]
   914  		}
   915  	}
   916  	return errors.New(aiss3.ErrPrefix + "[" + strings.TrimSuffix(msg, ".") + "]")
   917  }