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 }