github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/gateway/operations/getobject.go (about)

     1  package operations
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/treeverse/lakefs/pkg/block"
    13  	"github.com/treeverse/lakefs/pkg/catalog"
    14  	gatewayerrors "github.com/treeverse/lakefs/pkg/gateway/errors"
    15  	"github.com/treeverse/lakefs/pkg/gateway/path"
    16  	"github.com/treeverse/lakefs/pkg/gateway/serde"
    17  	"github.com/treeverse/lakefs/pkg/graveler"
    18  	"github.com/treeverse/lakefs/pkg/httputil"
    19  	"github.com/treeverse/lakefs/pkg/logging"
    20  	"github.com/treeverse/lakefs/pkg/permissions"
    21  )
    22  
    23  const (
    24  	QueryParamMaxParts = "max-parts"
    25  	// QueryParamPartNumberMarker Specifies the part after which listing should begin. Only parts with higher part numbers will be listed.
    26  	QueryParamPartNumberMarker       = "part-number-marker"
    27  	s3RedirectionSupportUserAgentTag = "s3RedirectionSupport"
    28  )
    29  
    30  type GetObject struct{}
    31  
    32  func (controller *GetObject) RequiredPermissions(_ *http.Request, repoID, _, path string) (permissions.Node, error) {
    33  	return permissions.Node{
    34  		Permission: permissions.Permission{
    35  			Action:   permissions.ReadObjectAction,
    36  			Resource: permissions.ObjectArn(repoID, path),
    37  		},
    38  	}, nil
    39  }
    40  
    41  func (controller *GetObject) Handle(w http.ResponseWriter, req *http.Request, o *PathOperation) {
    42  	if o.HandleUnsupported(w, req, "torrent", "acl", "retention", "legal-hold", "lambdaArn") {
    43  		return
    44  	}
    45  	userAgent := req.Header.Get("User-Agent")
    46  	redirect := strings.Contains(userAgent, s3RedirectionSupportUserAgentTag)
    47  	if redirect {
    48  		req = req.WithContext(logging.AddFields(req.Context(), logging.Fields{"S3_redirect": true}))
    49  	}
    50  	o.Incr("get_object", o.Principal, o.Repository.Name, o.Reference)
    51  	ctx := req.Context()
    52  	query := req.URL.Query()
    53  	if _, exists := query["versioning"]; exists {
    54  		o.EncodeResponse(w, req, serde.VersioningConfiguration{}, http.StatusOK)
    55  		return
    56  	}
    57  
    58  	if _, exists := query["tagging"]; exists {
    59  		o.EncodeResponse(w, req, serde.Tagging{}, http.StatusOK)
    60  		return
    61  	}
    62  
    63  	// check if this is a list parts call
    64  	if query.Has(QueryParamUploadID) {
    65  		handleListParts(w, req, o)
    66  		return
    67  	}
    68  
    69  	beforeMeta := time.Now()
    70  	entry, err := o.Catalog.GetEntry(ctx, o.Repository.Name, o.Reference, o.Path, catalog.GetEntryParams{})
    71  	metaTook := time.Since(beforeMeta)
    72  	o.Log(req).
    73  		WithField("took", metaTook).
    74  		WithError(err).
    75  		Debug("metadata operation to retrieve object done")
    76  
    77  	if errors.Is(err, graveler.ErrNotFound) {
    78  		// TODO: create distinction between missing repo & missing key
    79  		_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrNoSuchKey))
    80  		return
    81  	}
    82  	if errors.Is(err, catalog.ErrExpired) {
    83  		_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrNoSuchVersion))
    84  		return
    85  	}
    86  	if err != nil {
    87  		_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrInternalError))
    88  		return
    89  	}
    90  
    91  	// TODO: the rest of https://docs.aws.amazon.com/en_pv/AmazonS3/latest/API/API_GetObject.html
    92  	// range query
    93  	var data io.ReadCloser
    94  	var rng httputil.Range
    95  	// range query
    96  	rangeSpec := req.Header.Get("Range")
    97  	if len(rangeSpec) > 0 {
    98  		rng, err = httputil.ParseRange(rangeSpec, entry.Size)
    99  		if err != nil {
   100  			o.Log(req).WithError(err).WithField("range", rangeSpec).Debug("invalid range spec")
   101  			if errors.Is(err, httputil.ErrUnsatisfiableRange) {
   102  				_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrInvalidRange))
   103  				return
   104  			}
   105  		}
   106  		// by here, we have a range we can use.
   107  	}
   108  
   109  	statusCode := http.StatusOK
   110  	contentLength := entry.Size
   111  	contentRange := ""
   112  	objectPointer := block.ObjectPointer{
   113  		StorageNamespace: o.Repository.StorageNamespace,
   114  		IdentifierType:   entry.AddressType.ToIdentifierType(),
   115  		Identifier:       entry.PhysicalAddress,
   116  	}
   117  
   118  	if redirect {
   119  		preSignedURL, _, err := o.BlockStore.GetPreSignedURL(ctx, objectPointer, block.PreSignModeRead)
   120  		if err != nil {
   121  			code := gatewayerrors.ErrInternalError
   122  			_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(code))
   123  			return
   124  		}
   125  
   126  		o.SetHeader(w, "Location", preSignedURL)
   127  		w.WriteHeader(http.StatusTemporaryRedirect)
   128  		return
   129  	}
   130  
   131  	if rangeSpec == "" || err != nil {
   132  		// assemble a response body (range-less query)
   133  		data, err = o.BlockStore.Get(ctx, objectPointer, entry.Size)
   134  	} else {
   135  		contentLength = rng.Size()
   136  		contentRange = fmt.Sprintf("bytes %d-%d/%d", rng.StartOffset, rng.EndOffset, entry.Size)
   137  		statusCode = http.StatusPartialContent
   138  		data, err = o.BlockStore.GetRange(ctx, objectPointer, rng.StartOffset, rng.EndOffset)
   139  	}
   140  	if err != nil {
   141  		code := gatewayerrors.ErrInternalError
   142  		if errors.Is(err, block.ErrDataNotFound) {
   143  			code = gatewayerrors.ErrNoSuchVersion
   144  		}
   145  		_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(code))
   146  		return
   147  	}
   148  
   149  	o.SetHeader(w, "Last-Modified", httputil.HeaderTimestamp(entry.CreationDate))
   150  	o.SetHeader(w, "ETag", httputil.ETag(entry.Checksum))
   151  	o.SetHeader(w, "Content-Type", entry.ContentType)
   152  	o.SetHeader(w, "Accept-Ranges", "bytes")
   153  	if contentRange != "" {
   154  		o.SetHeader(w, "Content-Range", contentRange)
   155  	}
   156  	o.SetHeader(w, "Content-Length", fmt.Sprintf("%d", contentLength))
   157  	o.SetHeader(w, "X-Content-Type-Options", "nosniff")
   158  	o.SetHeader(w, "X-Frame-Options", "SAMEORIGIN")
   159  	o.SetHeader(w, "Content-Security-Policy", "default-src 'none'")
   160  	amzMetaWriteHeaders(w, entry.Metadata)
   161  	w.WriteHeader(statusCode)
   162  
   163  	defer func() {
   164  		_ = data.Close()
   165  	}()
   166  	_, err = io.Copy(w, data)
   167  	if err != nil {
   168  		o.Log(req).WithError(err).Error("could not write response body for object")
   169  	}
   170  }
   171  
   172  func handleListParts(w http.ResponseWriter, req *http.Request, o *PathOperation) {
   173  	o.Incr("list_mpu_parts", o.Principal, o.Repository.Name, o.Reference)
   174  	query := req.URL.Query()
   175  	uploadID := query.Get(QueryParamUploadID)
   176  	maxPartsStr := query.Get(QueryParamMaxParts)
   177  	partNumberMarker := query.Get(QueryParamPartNumberMarker)
   178  	resp := &serde.ListPartsOutput{
   179  		Bucket: o.Repository.Name,
   180  		Key:    path.WithRef(o.Path, o.Reference),
   181  	}
   182  	opts := block.ListPartsOpts{}
   183  	if maxPartsStr != "" {
   184  		maxParts, err := strconv.ParseInt(maxPartsStr, 10, 32)
   185  		if err != nil {
   186  			o.Log(req).WithField("uploadId", uploadID).
   187  				WithField("MaxParts", maxPartsStr).
   188  				WithError(err).Error("malformed query parameter 'MaxParts'")
   189  			_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrInternalError))
   190  			return
   191  		}
   192  		resp.MaxParts = int32(maxParts)
   193  		opts.MaxParts = &resp.MaxParts
   194  	}
   195  	if partNumberMarker != "" {
   196  		opts.PartNumberMarker = &partNumberMarker
   197  	}
   198  
   199  	req = req.WithContext(logging.AddFields(req.Context(), logging.Fields{
   200  		logging.UploadIDFieldKey: uploadID,
   201  	}))
   202  
   203  	multiPart, err := o.MultipartTracker.Get(req.Context(), uploadID)
   204  	if err != nil {
   205  		o.Log(req).WithError(err).Error("could not read multipart record")
   206  		_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrInternalError))
   207  		return
   208  	}
   209  
   210  	partsResp, err := o.BlockStore.ListParts(req.Context(), block.ObjectPointer{
   211  		StorageNamespace: o.Repository.StorageNamespace,
   212  		IdentifierType:   block.IdentifierTypeRelative,
   213  		Identifier:       multiPart.PhysicalAddress,
   214  	}, uploadID, opts)
   215  	if err != nil {
   216  		o.Log(req).WithField("uploadId", uploadID).
   217  			WithError(err).Error("list parts failed")
   218  		_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrInternalError))
   219  		return
   220  	}
   221  	parts := make([]serde.MultipartUploadPart, len(partsResp.Parts))
   222  	for i, part := range partsResp.Parts {
   223  		parts[i] = serde.MultipartUploadPart{
   224  			PartNumber:   int32(part.PartNumber),
   225  			ETag:         part.ETag,
   226  			LastModified: serde.Timestamp(part.LastModified),
   227  			Size:         part.Size,
   228  		}
   229  	}
   230  	resp.IsTruncated = partsResp.IsTruncated
   231  	resp.Parts = parts
   232  	if partsResp.NextPartNumberMarker != nil {
   233  		marker, err := strconv.ParseInt(*partsResp.NextPartNumberMarker, 10, 32)
   234  		if err != nil {
   235  			o.Log(req).WithField("uploadId", uploadID).
   236  				WithField("NextPartNumberMarker", partsResp.NextPartNumberMarker).
   237  				WithError(err).Error("invalid response 'NextPartNumberMarker'")
   238  			_ = o.EncodeError(w, req, err, gatewayerrors.Codes.ToAPIErr(gatewayerrors.ErrInternalError))
   239  			return
   240  		}
   241  		resp.NextPartNumberMarker = int32(marker)
   242  	}
   243  
   244  	o.EncodeResponse(w, req, resp, http.StatusOK)
   245  }