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

     1  package operations
     2  
     3  import (
     4  	"encoding/xml"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/treeverse/lakefs/pkg/block"
    13  	gatewayErrors "github.com/treeverse/lakefs/pkg/gateway/errors"
    14  	"github.com/treeverse/lakefs/pkg/gateway/multipart"
    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  	CreateMultipartUploadQueryParam   = "uploads"
    25  	CompleteMultipartUploadQueryParam = "uploadId"
    26  )
    27  
    28  type PostObject struct{}
    29  
    30  func (controller *PostObject) RequiredPermissions(_ *http.Request, repoID, _, path string) (permissions.Node, error) {
    31  	return permissions.Node{
    32  		Permission: permissions.Permission{
    33  			Action:   permissions.WriteObjectAction,
    34  			Resource: permissions.ObjectArn(repoID, path),
    35  		},
    36  	}, nil
    37  }
    38  
    39  func (controller *PostObject) HandleCreateMultipartUpload(w http.ResponseWriter, req *http.Request, o *PathOperation) {
    40  	o.Incr("create_mpu", o.Principal, o.Repository.Name, o.Reference)
    41  	branchExists, err := o.Catalog.BranchExists(req.Context(), o.Repository.Name, o.Reference)
    42  	if err != nil {
    43  		o.Log(req).WithError(err).Error("could not check if branch exists")
    44  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
    45  		return
    46  	}
    47  	if !branchExists {
    48  		o.Log(req).Debug("branch not found")
    49  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrNoSuchBucket))
    50  		return
    51  	}
    52  	address := o.PathProvider.NewPath()
    53  	storageClass := StorageClassFromHeader(req.Header)
    54  	opts := block.CreateMultiPartUploadOpts{StorageClass: storageClass}
    55  	resp, err := o.BlockStore.CreateMultiPartUpload(req.Context(), block.ObjectPointer{
    56  		StorageNamespace: o.Repository.StorageNamespace,
    57  		IdentifierType:   block.IdentifierTypeRelative,
    58  		Identifier:       address,
    59  	}, req, opts)
    60  	if err != nil {
    61  		o.Log(req).WithError(err).Error("could not create multipart upload")
    62  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
    63  		return
    64  	}
    65  	mpu := multipart.Upload{
    66  		UploadID:        resp.UploadID,
    67  		Path:            o.Path,
    68  		CreationDate:    time.Now(),
    69  		PhysicalAddress: address,
    70  		Metadata:        map[string]string(amzMetaAsMetadata(req)),
    71  		ContentType:     req.Header.Get("Content-Type"),
    72  	}
    73  	err = o.MultipartTracker.Create(req.Context(), mpu)
    74  	if err != nil {
    75  		o.Log(req).WithError(err).Error("could not write multipart upload to DB")
    76  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
    77  		return
    78  	}
    79  	o.SetHeaders(w, resp.ServerSideHeader)
    80  	o.EncodeResponse(w, req, &serde.InitiateMultipartUploadResult{
    81  		Bucket:   o.Repository.Name,
    82  		Key:      path.WithRef(o.Path, o.Reference),
    83  		UploadID: resp.UploadID,
    84  	}, http.StatusOK)
    85  }
    86  
    87  func (controller *PostObject) HandleCompleteMultipartUpload(w http.ResponseWriter, req *http.Request, o *PathOperation) {
    88  	o.Incr("complete_mpu", o.Principal, o.Repository.Name, o.Reference)
    89  	uploadID := req.URL.Query().Get(CompleteMultipartUploadQueryParam)
    90  	req = req.WithContext(logging.AddFields(req.Context(), logging.Fields{logging.UploadIDFieldKey: uploadID}))
    91  	multiPart, err := o.MultipartTracker.Get(req.Context(), uploadID)
    92  	if err != nil {
    93  		o.Log(req).WithError(err).Error("could not read multipart record")
    94  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
    95  		return
    96  	}
    97  	objName := multiPart.PhysicalAddress
    98  	req = req.WithContext(logging.AddFields(req.Context(), logging.Fields{logging.PhysicalAddressFieldKey: objName}))
    99  	xmlMultipartComplete, err := io.ReadAll(req.Body)
   100  	if err != nil {
   101  		o.Log(req).WithError(err).Error("could not read request body")
   102  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
   103  		return
   104  	}
   105  	var multipartList block.MultipartUploadCompletion
   106  	err = xml.Unmarshal(xmlMultipartComplete, &multipartList)
   107  	if err != nil {
   108  		o.Log(req).WithError(err).Error("could not parse multipart XML on complete multipart")
   109  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
   110  		return
   111  	}
   112  	normalizeMultipartUploadCompletion(&multipartList)
   113  	resp, err := o.BlockStore.CompleteMultiPartUpload(req.Context(),
   114  		block.ObjectPointer{
   115  			StorageNamespace: o.Repository.StorageNamespace,
   116  			IdentifierType:   block.IdentifierTypeRelative,
   117  			Identifier:       objName,
   118  		},
   119  		uploadID,
   120  		&multipartList)
   121  	if err != nil {
   122  		o.Log(req).WithError(err).Error("could not complete multipart upload")
   123  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
   124  		return
   125  	}
   126  	checksum := strings.Split(resp.ETag, "-")[0]
   127  	err = o.finishUpload(req, checksum, objName, resp.ContentLength, true, multiPart.Metadata, multiPart.ContentType)
   128  	if errors.Is(err, graveler.ErrWriteToProtectedBranch) {
   129  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrWriteToProtectedBranch))
   130  		return
   131  	}
   132  	if errors.Is(err, graveler.ErrReadOnlyRepository) {
   133  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrReadOnlyRepository))
   134  		return
   135  	}
   136  	if err != nil {
   137  		_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrInternalError))
   138  		return
   139  	}
   140  	err = o.MultipartTracker.Delete(req.Context(), uploadID)
   141  	if err != nil {
   142  		o.Log(req).WithError(err).Warn("could not delete multipart record")
   143  	}
   144  
   145  	scheme := httputil.RequestScheme(req)
   146  	var location string
   147  	if o.MatchedHost {
   148  		location = fmt.Sprintf("%s://%s/%s/%s", scheme, req.Host, o.Reference, o.Path)
   149  	} else {
   150  		location = fmt.Sprintf("%s://%s/%s/%s/%s", scheme, req.Host, o.Repository.Name, o.Reference, o.Path)
   151  	}
   152  	o.SetHeaders(w, resp.ServerSideHeader)
   153  	o.EncodeResponse(w, req, &serde.CompleteMultipartUploadResult{
   154  		Location: location,
   155  		Bucket:   o.Repository.Name,
   156  		Key:      path.WithRef(o.Path, o.Reference),
   157  		ETag:     httputil.ETag(resp.ETag),
   158  	}, http.StatusOK)
   159  }
   160  
   161  // normalizeMultipartUploadCompletion normalization incoming multipart upload completion list.
   162  // we make sure that each part's ETag will be without the wrapping quotes
   163  func normalizeMultipartUploadCompletion(list *block.MultipartUploadCompletion) {
   164  	for i := range list.Part {
   165  		list.Part[i].ETag = strings.Trim(list.Part[i].ETag, `"`)
   166  	}
   167  }
   168  
   169  func (controller *PostObject) Handle(w http.ResponseWriter, req *http.Request, o *PathOperation) {
   170  	if o.HandleUnsupported(w, req, "select", "restore") {
   171  		return
   172  	}
   173  
   174  	// POST is only supported for CreateMultipartUpload/CompleteMultipartUpload
   175  	// https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html
   176  	// https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html
   177  	query := req.URL.Query()
   178  	switch {
   179  	case query.Has(CreateMultipartUploadQueryParam):
   180  		controller.HandleCreateMultipartUpload(w, req, o)
   181  	case query.Has(CompleteMultipartUploadQueryParam):
   182  		controller.HandleCompleteMultipartUpload(w, req, o)
   183  	default:
   184  		w.WriteHeader(http.StatusMethodNotAllowed)
   185  	}
   186  }