api-get-object.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. /*
  2. * Minio Go Library for Amazon S3 Compatible Cloud Storage
  3. * Copyright 2015-2017 Minio, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package minio
  18. import (
  19. "context"
  20. "errors"
  21. "fmt"
  22. "io"
  23. "net/http"
  24. "strings"
  25. "sync"
  26. "time"
  27. "github.com/minio/minio-go/pkg/encrypt"
  28. "github.com/minio/minio-go/pkg/s3utils"
  29. )
  30. // GetEncryptedObject deciphers and streams data stored in the server after applying a specified encryption materials,
  31. // returned stream should be closed by the caller.
  32. func (c Client) GetEncryptedObject(bucketName, objectName string, encryptMaterials encrypt.Materials) (io.ReadCloser, error) {
  33. if encryptMaterials == nil {
  34. return nil, ErrInvalidArgument("Unable to recognize empty encryption properties")
  35. }
  36. return c.GetObject(bucketName, objectName, GetObjectOptions{Materials: encryptMaterials})
  37. }
  38. // GetObject - returns an seekable, readable object.
  39. func (c Client) GetObject(bucketName, objectName string, opts GetObjectOptions) (*Object, error) {
  40. return c.getObjectWithContext(context.Background(), bucketName, objectName, opts)
  41. }
  42. // GetObject wrapper function that accepts a request context
  43. func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (*Object, error) {
  44. // Input validation.
  45. if err := s3utils.CheckValidBucketName(bucketName); err != nil {
  46. return nil, err
  47. }
  48. if err := s3utils.CheckValidObjectName(objectName); err != nil {
  49. return nil, err
  50. }
  51. var httpReader io.ReadCloser
  52. var objectInfo ObjectInfo
  53. var err error
  54. // Create request channel.
  55. reqCh := make(chan getRequest)
  56. // Create response channel.
  57. resCh := make(chan getResponse)
  58. // Create done channel.
  59. doneCh := make(chan struct{})
  60. // This routine feeds partial object data as and when the caller reads.
  61. go func() {
  62. defer close(reqCh)
  63. defer close(resCh)
  64. // Used to verify if etag of object has changed since last read.
  65. var etag string
  66. // Loop through the incoming control messages and read data.
  67. for {
  68. select {
  69. // When the done channel is closed exit our routine.
  70. case <-doneCh:
  71. // Close the http response body before returning.
  72. // This ends the connection with the server.
  73. if httpReader != nil {
  74. httpReader.Close()
  75. }
  76. return
  77. // Gather incoming request.
  78. case req := <-reqCh:
  79. // If this is the first request we may not need to do a getObject request yet.
  80. if req.isFirstReq {
  81. // First request is a Read/ReadAt.
  82. if req.isReadOp {
  83. // Differentiate between wanting the whole object and just a range.
  84. if req.isReadAt {
  85. // If this is a ReadAt request only get the specified range.
  86. // Range is set with respect to the offset and length of the buffer requested.
  87. // Do not set objectInfo from the first readAt request because it will not get
  88. // the whole object.
  89. opts.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1)
  90. } else if req.Offset > 0 {
  91. opts.SetRange(req.Offset, 0)
  92. }
  93. httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, opts)
  94. if err != nil {
  95. resCh <- getResponse{Error: err}
  96. return
  97. }
  98. etag = objectInfo.ETag
  99. // Read at least firstReq.Buffer bytes, if not we have
  100. // reached our EOF.
  101. size, err := io.ReadFull(httpReader, req.Buffer)
  102. if size > 0 && err == io.ErrUnexpectedEOF {
  103. // If an EOF happens after reading some but not
  104. // all the bytes ReadFull returns ErrUnexpectedEOF
  105. err = io.EOF
  106. }
  107. // Send back the first response.
  108. resCh <- getResponse{
  109. objectInfo: objectInfo,
  110. Size: int(size),
  111. Error: err,
  112. didRead: true,
  113. }
  114. } else {
  115. // First request is a Stat or Seek call.
  116. // Only need to run a StatObject until an actual Read or ReadAt request comes through.
  117. objectInfo, err = c.statObject(ctx, bucketName, objectName, StatObjectOptions{opts})
  118. if err != nil {
  119. resCh <- getResponse{
  120. Error: err,
  121. }
  122. // Exit the go-routine.
  123. return
  124. }
  125. etag = objectInfo.ETag
  126. // Send back the first response.
  127. resCh <- getResponse{
  128. objectInfo: objectInfo,
  129. }
  130. }
  131. } else if req.settingObjectInfo { // Request is just to get objectInfo.
  132. if etag != "" {
  133. opts.SetMatchETag(etag)
  134. }
  135. objectInfo, err := c.statObject(ctx, bucketName, objectName, StatObjectOptions{opts})
  136. if err != nil {
  137. resCh <- getResponse{
  138. Error: err,
  139. }
  140. // Exit the goroutine.
  141. return
  142. }
  143. // Send back the objectInfo.
  144. resCh <- getResponse{
  145. objectInfo: objectInfo,
  146. }
  147. } else {
  148. // Offset changes fetch the new object at an Offset.
  149. // Because the httpReader may not be set by the first
  150. // request if it was a stat or seek it must be checked
  151. // if the object has been read or not to only initialize
  152. // new ones when they haven't been already.
  153. // All readAt requests are new requests.
  154. if req.DidOffsetChange || !req.beenRead {
  155. if etag != "" {
  156. opts.SetMatchETag(etag)
  157. }
  158. if httpReader != nil {
  159. // Close previously opened http reader.
  160. httpReader.Close()
  161. }
  162. // If this request is a readAt only get the specified range.
  163. if req.isReadAt {
  164. // Range is set with respect to the offset and length of the buffer requested.
  165. opts.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1)
  166. } else if req.Offset > 0 { // Range is set with respect to the offset.
  167. opts.SetRange(req.Offset, 0)
  168. }
  169. httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, opts)
  170. if err != nil {
  171. resCh <- getResponse{
  172. Error: err,
  173. }
  174. return
  175. }
  176. }
  177. // Read at least req.Buffer bytes, if not we have
  178. // reached our EOF.
  179. size, err := io.ReadFull(httpReader, req.Buffer)
  180. if err == io.ErrUnexpectedEOF {
  181. // If an EOF happens after reading some but not
  182. // all the bytes ReadFull returns ErrUnexpectedEOF
  183. err = io.EOF
  184. }
  185. // Reply back how much was read.
  186. resCh <- getResponse{
  187. Size: int(size),
  188. Error: err,
  189. didRead: true,
  190. objectInfo: objectInfo,
  191. }
  192. }
  193. }
  194. }
  195. }()
  196. // Create a newObject through the information sent back by reqCh.
  197. return newObject(reqCh, resCh, doneCh), nil
  198. }
  199. // get request message container to communicate with internal
  200. // go-routine.
  201. type getRequest struct {
  202. Buffer []byte
  203. Offset int64 // readAt offset.
  204. DidOffsetChange bool // Tracks the offset changes for Seek requests.
  205. beenRead bool // Determines if this is the first time an object is being read.
  206. isReadAt bool // Determines if this request is a request to a specific range
  207. isReadOp bool // Determines if this request is a Read or Read/At request.
  208. isFirstReq bool // Determines if this request is the first time an object is being accessed.
  209. settingObjectInfo bool // Determines if this request is to set the objectInfo of an object.
  210. }
  211. // get response message container to reply back for the request.
  212. type getResponse struct {
  213. Size int
  214. Error error
  215. didRead bool // Lets subsequent calls know whether or not httpReader has been initiated.
  216. objectInfo ObjectInfo // Used for the first request.
  217. }
  218. // Object represents an open object. It implements
  219. // Reader, ReaderAt, Seeker, Closer for a HTTP stream.
  220. type Object struct {
  221. // Mutex.
  222. mutex *sync.Mutex
  223. // User allocated and defined.
  224. reqCh chan<- getRequest
  225. resCh <-chan getResponse
  226. doneCh chan<- struct{}
  227. currOffset int64
  228. objectInfo ObjectInfo
  229. // Ask lower level to initiate data fetching based on currOffset
  230. seekData bool
  231. // Keeps track of closed call.
  232. isClosed bool
  233. // Keeps track of if this is the first call.
  234. isStarted bool
  235. // Previous error saved for future calls.
  236. prevErr error
  237. // Keeps track of if this object has been read yet.
  238. beenRead bool
  239. // Keeps track of if objectInfo has been set yet.
  240. objectInfoSet bool
  241. }
  242. // doGetRequest - sends and blocks on the firstReqCh and reqCh of an object.
  243. // Returns back the size of the buffer read, if anything was read, as well
  244. // as any error encountered. For all first requests sent on the object
  245. // it is also responsible for sending back the objectInfo.
  246. func (o *Object) doGetRequest(request getRequest) (getResponse, error) {
  247. o.reqCh <- request
  248. response := <-o.resCh
  249. // Return any error to the top level.
  250. if response.Error != nil {
  251. return response, response.Error
  252. }
  253. // This was the first request.
  254. if !o.isStarted {
  255. // The object has been operated on.
  256. o.isStarted = true
  257. }
  258. // Set the objectInfo if the request was not readAt
  259. // and it hasn't been set before.
  260. if !o.objectInfoSet && !request.isReadAt {
  261. o.objectInfo = response.objectInfo
  262. o.objectInfoSet = true
  263. }
  264. // Set beenRead only if it has not been set before.
  265. if !o.beenRead {
  266. o.beenRead = response.didRead
  267. }
  268. // Data are ready on the wire, no need to reinitiate connection in lower level
  269. o.seekData = false
  270. return response, nil
  271. }
  272. // setOffset - handles the setting of offsets for
  273. // Read/ReadAt/Seek requests.
  274. func (o *Object) setOffset(bytesRead int64) error {
  275. // Update the currentOffset.
  276. o.currOffset += bytesRead
  277. if o.objectInfo.Size > -1 && o.currOffset >= o.objectInfo.Size {
  278. return io.EOF
  279. }
  280. return nil
  281. }
  282. // Read reads up to len(b) bytes into b. It returns the number of
  283. // bytes read (0 <= n <= len(b)) and any error encountered. Returns
  284. // io.EOF upon end of file.
  285. func (o *Object) Read(b []byte) (n int, err error) {
  286. if o == nil {
  287. return 0, ErrInvalidArgument("Object is nil")
  288. }
  289. // Locking.
  290. o.mutex.Lock()
  291. defer o.mutex.Unlock()
  292. // prevErr is previous error saved from previous operation.
  293. if o.prevErr != nil || o.isClosed {
  294. return 0, o.prevErr
  295. }
  296. // Create a new request.
  297. readReq := getRequest{
  298. isReadOp: true,
  299. beenRead: o.beenRead,
  300. Buffer: b,
  301. }
  302. // Alert that this is the first request.
  303. if !o.isStarted {
  304. readReq.isFirstReq = true
  305. }
  306. // Ask to establish a new data fetch routine based on seekData flag
  307. readReq.DidOffsetChange = o.seekData
  308. readReq.Offset = o.currOffset
  309. // Send and receive from the first request.
  310. response, err := o.doGetRequest(readReq)
  311. if err != nil && err != io.EOF {
  312. // Save the error for future calls.
  313. o.prevErr = err
  314. return response.Size, err
  315. }
  316. // Bytes read.
  317. bytesRead := int64(response.Size)
  318. // Set the new offset.
  319. oerr := o.setOffset(bytesRead)
  320. if oerr != nil {
  321. // Save the error for future calls.
  322. o.prevErr = oerr
  323. return response.Size, oerr
  324. }
  325. // Return the response.
  326. return response.Size, err
  327. }
  328. // Stat returns the ObjectInfo structure describing Object.
  329. func (o *Object) Stat() (ObjectInfo, error) {
  330. if o == nil {
  331. return ObjectInfo{}, ErrInvalidArgument("Object is nil")
  332. }
  333. // Locking.
  334. o.mutex.Lock()
  335. defer o.mutex.Unlock()
  336. if o.prevErr != nil && o.prevErr != io.EOF || o.isClosed {
  337. return ObjectInfo{}, o.prevErr
  338. }
  339. // This is the first request.
  340. if !o.isStarted || !o.objectInfoSet {
  341. statReq := getRequest{
  342. isFirstReq: !o.isStarted,
  343. settingObjectInfo: !o.objectInfoSet,
  344. }
  345. // Send the request and get the response.
  346. _, err := o.doGetRequest(statReq)
  347. if err != nil {
  348. o.prevErr = err
  349. return ObjectInfo{}, err
  350. }
  351. }
  352. return o.objectInfo, nil
  353. }
  354. // ReadAt reads len(b) bytes from the File starting at byte offset
  355. // off. It returns the number of bytes read and the error, if any.
  356. // ReadAt always returns a non-nil error when n < len(b). At end of
  357. // file, that error is io.EOF.
  358. func (o *Object) ReadAt(b []byte, offset int64) (n int, err error) {
  359. if o == nil {
  360. return 0, ErrInvalidArgument("Object is nil")
  361. }
  362. // Locking.
  363. o.mutex.Lock()
  364. defer o.mutex.Unlock()
  365. // prevErr is error which was saved in previous operation.
  366. if o.prevErr != nil || o.isClosed {
  367. return 0, o.prevErr
  368. }
  369. // Can only compare offsets to size when size has been set.
  370. if o.objectInfoSet {
  371. // If offset is negative than we return io.EOF.
  372. // If offset is greater than or equal to object size we return io.EOF.
  373. if (o.objectInfo.Size > -1 && offset >= o.objectInfo.Size) || offset < 0 {
  374. return 0, io.EOF
  375. }
  376. }
  377. // Create the new readAt request.
  378. readAtReq := getRequest{
  379. isReadOp: true,
  380. isReadAt: true,
  381. DidOffsetChange: true, // Offset always changes.
  382. beenRead: o.beenRead, // Set if this is the first request to try and read.
  383. Offset: offset, // Set the offset.
  384. Buffer: b,
  385. }
  386. // Alert that this is the first request.
  387. if !o.isStarted {
  388. readAtReq.isFirstReq = true
  389. }
  390. // Send and receive from the first request.
  391. response, err := o.doGetRequest(readAtReq)
  392. if err != nil && err != io.EOF {
  393. // Save the error.
  394. o.prevErr = err
  395. return response.Size, err
  396. }
  397. // Bytes read.
  398. bytesRead := int64(response.Size)
  399. // There is no valid objectInfo yet
  400. // to compare against for EOF.
  401. if !o.objectInfoSet {
  402. // Update the currentOffset.
  403. o.currOffset += bytesRead
  404. } else {
  405. // If this was not the first request update
  406. // the offsets and compare against objectInfo
  407. // for EOF.
  408. oerr := o.setOffset(bytesRead)
  409. if oerr != nil {
  410. o.prevErr = oerr
  411. return response.Size, oerr
  412. }
  413. }
  414. return response.Size, err
  415. }
  416. // Seek sets the offset for the next Read or Write to offset,
  417. // interpreted according to whence: 0 means relative to the
  418. // origin of the file, 1 means relative to the current offset,
  419. // and 2 means relative to the end.
  420. // Seek returns the new offset and an error, if any.
  421. //
  422. // Seeking to a negative offset is an error. Seeking to any positive
  423. // offset is legal, subsequent io operations succeed until the
  424. // underlying object is not closed.
  425. func (o *Object) Seek(offset int64, whence int) (n int64, err error) {
  426. if o == nil {
  427. return 0, ErrInvalidArgument("Object is nil")
  428. }
  429. // Locking.
  430. o.mutex.Lock()
  431. defer o.mutex.Unlock()
  432. if o.prevErr != nil {
  433. // At EOF seeking is legal allow only io.EOF, for any other errors we return.
  434. if o.prevErr != io.EOF {
  435. return 0, o.prevErr
  436. }
  437. }
  438. // Negative offset is valid for whence of '2'.
  439. if offset < 0 && whence != 2 {
  440. return 0, ErrInvalidArgument(fmt.Sprintf("Negative position not allowed for %d.", whence))
  441. }
  442. // This is the first request. So before anything else
  443. // get the ObjectInfo.
  444. if !o.isStarted || !o.objectInfoSet {
  445. // Create the new Seek request.
  446. seekReq := getRequest{
  447. isReadOp: false,
  448. Offset: offset,
  449. isFirstReq: true,
  450. }
  451. // Send and receive from the seek request.
  452. _, err := o.doGetRequest(seekReq)
  453. if err != nil {
  454. // Save the error.
  455. o.prevErr = err
  456. return 0, err
  457. }
  458. }
  459. // Switch through whence.
  460. switch whence {
  461. default:
  462. return 0, ErrInvalidArgument(fmt.Sprintf("Invalid whence %d", whence))
  463. case 0:
  464. if o.objectInfo.Size > -1 && offset > o.objectInfo.Size {
  465. return 0, io.EOF
  466. }
  467. o.currOffset = offset
  468. case 1:
  469. if o.objectInfo.Size > -1 && o.currOffset+offset > o.objectInfo.Size {
  470. return 0, io.EOF
  471. }
  472. o.currOffset += offset
  473. case 2:
  474. // If we don't know the object size return an error for io.SeekEnd
  475. if o.objectInfo.Size < 0 {
  476. return 0, ErrInvalidArgument("Whence END is not supported when the object size is unknown")
  477. }
  478. // Seeking to positive offset is valid for whence '2', but
  479. // since we are backing a Reader we have reached 'EOF' if
  480. // offset is positive.
  481. if offset > 0 {
  482. return 0, io.EOF
  483. }
  484. // Seeking to negative position not allowed for whence.
  485. if o.objectInfo.Size+offset < 0 {
  486. return 0, ErrInvalidArgument(fmt.Sprintf("Seeking at negative offset not allowed for %d", whence))
  487. }
  488. o.currOffset = o.objectInfo.Size + offset
  489. }
  490. // Reset the saved error since we successfully seeked, let the Read
  491. // and ReadAt decide.
  492. if o.prevErr == io.EOF {
  493. o.prevErr = nil
  494. }
  495. // Ask lower level to fetch again from source
  496. o.seekData = true
  497. // Return the effective offset.
  498. return o.currOffset, nil
  499. }
  500. // Close - The behavior of Close after the first call returns error
  501. // for subsequent Close() calls.
  502. func (o *Object) Close() (err error) {
  503. if o == nil {
  504. return ErrInvalidArgument("Object is nil")
  505. }
  506. // Locking.
  507. o.mutex.Lock()
  508. defer o.mutex.Unlock()
  509. // if already closed return an error.
  510. if o.isClosed {
  511. return o.prevErr
  512. }
  513. // Close successfully.
  514. close(o.doneCh)
  515. // Save for future operations.
  516. errMsg := "Object is already closed. Bad file descriptor."
  517. o.prevErr = errors.New(errMsg)
  518. // Save here that we closed done channel successfully.
  519. o.isClosed = true
  520. return nil
  521. }
  522. // newObject instantiates a new *minio.Object*
  523. // ObjectInfo will be set by setObjectInfo
  524. func newObject(reqCh chan<- getRequest, resCh <-chan getResponse, doneCh chan<- struct{}) *Object {
  525. return &Object{
  526. mutex: &sync.Mutex{},
  527. reqCh: reqCh,
  528. resCh: resCh,
  529. doneCh: doneCh,
  530. }
  531. }
  532. // getObject - retrieve object from Object Storage.
  533. //
  534. // Additionally this function also takes range arguments to download the specified
  535. // range bytes of an object. Setting offset and length = 0 will download the full object.
  536. //
  537. // For more information about the HTTP Range header.
  538. // go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.
  539. func (c Client) getObject(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (io.ReadCloser, ObjectInfo, error) {
  540. // Validate input arguments.
  541. if err := s3utils.CheckValidBucketName(bucketName); err != nil {
  542. return nil, ObjectInfo{}, err
  543. }
  544. if err := s3utils.CheckValidObjectName(objectName); err != nil {
  545. return nil, ObjectInfo{}, err
  546. }
  547. // Execute GET on objectName.
  548. resp, err := c.executeMethod(ctx, "GET", requestMetadata{
  549. bucketName: bucketName,
  550. objectName: objectName,
  551. customHeader: opts.Header(),
  552. contentSHA256Hex: emptySHA256Hex,
  553. })
  554. if err != nil {
  555. return nil, ObjectInfo{}, err
  556. }
  557. if resp != nil {
  558. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
  559. return nil, ObjectInfo{}, httpRespToErrorResponse(resp, bucketName, objectName)
  560. }
  561. }
  562. // Trim off the odd double quotes from ETag in the beginning and end.
  563. md5sum := strings.TrimPrefix(resp.Header.Get("ETag"), "\"")
  564. md5sum = strings.TrimSuffix(md5sum, "\"")
  565. // Parse the date.
  566. date, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified"))
  567. if err != nil {
  568. msg := "Last-Modified time format not recognized. " + reportIssue
  569. return nil, ObjectInfo{}, ErrorResponse{
  570. Code: "InternalError",
  571. Message: msg,
  572. RequestID: resp.Header.Get("x-amz-request-id"),
  573. HostID: resp.Header.Get("x-amz-id-2"),
  574. Region: resp.Header.Get("x-amz-bucket-region"),
  575. }
  576. }
  577. // Get content-type.
  578. contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
  579. if contentType == "" {
  580. contentType = "application/octet-stream"
  581. }
  582. objectStat := ObjectInfo{
  583. ETag: md5sum,
  584. Key: objectName,
  585. Size: resp.ContentLength,
  586. LastModified: date,
  587. ContentType: contentType,
  588. // Extract only the relevant header keys describing the object.
  589. // following function filters out a list of standard set of keys
  590. // which are not part of object metadata.
  591. Metadata: extractObjMetadata(resp.Header),
  592. }
  593. reader := resp.Body
  594. if opts.Materials != nil {
  595. err = opts.Materials.SetupDecryptMode(reader, objectStat.Metadata.Get(amzHeaderIV), objectStat.Metadata.Get(amzHeaderKey))
  596. if err != nil {
  597. return nil, ObjectInfo{}, err
  598. }
  599. reader = opts.Materials
  600. }
  601. // do not close body here, caller will close
  602. return reader, objectStat, nil
  603. }