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 }