Skip to content

Commit 802bd60

Browse files
authored
fix: getObject with range request and then stat would not return the correct range size (#2267)
1 parent fcd0259 commit 802bd60

3 files changed

Lines changed: 300 additions & 17 deletions

File tree

api-get-object.go

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
8181
reqCh := make(chan getRequest)
8282
// Create response channel.
8383
resCh := make(chan getResponse)
84+
// record original range header for stat operation.
85+
originalRangeHeader := opts.Header().Get("Range")
8486

8587
// This routine feeds partial object data as and when the caller reads.
8688
go func() {
@@ -147,19 +149,35 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
147149
// it to io.EOF - return unexpected EOF.
148150
err = io.ErrUnexpectedEOF
149151
}
152+
// when doing a readAt, we can't reuse the httpReader for next read action.
153+
if req.isReadAt {
154+
httpReader.Close()
155+
httpReader = nil
156+
}
157+
158+
objectSize := int64(0)
159+
// case for ReadFull with range request
160+
if !req.isReadAt && req.Offset == 0 {
161+
objectSize = objectInfo.Size
162+
}
150163
// Send back the first response.
151164
resCh <- getResponse{
165+
ObjectSize: objectSize,
152166
objectInfo: objectInfo,
153167
Size: size,
154168
Error: err,
155169
didRead: true,
156170
}
157171
} else {
158-
// First request is a Stat or Seek call.
159-
// Only need to run a StatObject until an actual Read or ReadAt request comes through.
172+
if originalRangeHeader == "" {
173+
// First request is a Seek call.
174+
// Only need to run a StatObject until an actual Read request comes through.
160175

161-
// Remove range header if already set, for stat Operations to get original file size.
162-
delete(opts.headers, "Range")
176+
// Remove range header if already set, for stat Operations to get original file size.
177+
delete(opts.headers, "Range")
178+
} else {
179+
opts.headers["Range"] = originalRangeHeader
180+
}
163181
objectInfo, err = c.StatObject(gctx, bucketName, objectName, StatObjectOptions(opts))
164182
if err != nil {
165183
resCh <- getResponse{
@@ -171,12 +189,17 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
171189
etag = objectInfo.ETag
172190
// Send back the first response.
173191
resCh <- getResponse{
192+
ObjectSize: objectInfo.Size,
174193
objectInfo: objectInfo,
175194
}
176195
}
177196
} else if req.settingObjectInfo { // Request is just to get objectInfo.
178197
// Remove range header if already set, for stat Operations to get original file size.
179-
delete(opts.headers, "Range")
198+
if originalRangeHeader == "" {
199+
delete(opts.headers, "Range")
200+
} else {
201+
opts.headers["Range"] = originalRangeHeader
202+
}
180203
// Check whether this is snowball
181204
// if yes do not use If-Match feature
182205
// it doesn't work.
@@ -193,9 +216,12 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
193216
}
194217
// Send back the objectInfo.
195218
resCh <- getResponse{
219+
ObjectSize: objectInfo.Size,
196220
objectInfo: objectInfo,
197221
}
198222
} else {
223+
objectSize := int64(0)
224+
renewReader := false
199225
// Offset changes fetch the new object at an Offset.
200226
// Because the httpReader may not be set by the first
201227
// request if it was a stat or seek it must be checked
@@ -223,8 +249,11 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
223249
} else if req.Offset > 0 { // Range is set with respect to the offset.
224250
opts.SetRange(req.Offset, 0)
225251
} else {
226-
// Remove range header if already set
227-
delete(opts.headers, "Range")
252+
if originalRangeHeader == "" {
253+
delete(opts.headers, "Range")
254+
} else {
255+
opts.headers["Range"] = originalRangeHeader
256+
}
228257
}
229258
httpReader, objectInfo, _, err = c.getObject(gctx, bucketName, objectName, opts)
230259
if err != nil {
@@ -239,6 +268,11 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
239268
}
240269
return
241270
}
271+
// case for ReadFull with range request
272+
if !req.isReadAt && req.Offset == 0 {
273+
objectSize = objectInfo.Size
274+
}
275+
renewReader = true
242276
totalRead = 0
243277
}
244278

@@ -265,9 +299,13 @@ func (c *Client) GetObject(ctx context.Context, bucketName, objectName string, o
265299
// it to io.EOF - return unexpected EOF.
266300
err = io.ErrUnexpectedEOF
267301
}
268-
302+
if renewReader && req.isReadAt {
303+
httpReader.Close()
304+
httpReader = nil
305+
}
269306
// Reply back how much was read.
270307
resCh <- getResponse{
308+
ObjectSize: objectSize,
271309
Size: size,
272310
Error: err,
273311
didRead: true,
@@ -296,6 +334,7 @@ type getRequest struct {
296334

297335
// get response message container to reply back for the request.
298336
type getResponse struct {
337+
ObjectSize int64
299338
Size int
300339
Error error
301340
didRead bool // Lets subsequent calls know whether or not httpReader has been initiated.
@@ -314,6 +353,7 @@ type Object struct {
314353
ctx context.Context
315354
cancel context.CancelFunc
316355
currOffset int64
356+
totalSize int64
317357
objectInfo ObjectInfo
318358

319359
// Ask lower level to initiate data fetching based on currOffset
@@ -371,6 +411,10 @@ func (o *Object) doGetRequest(request getRequest) (getResponse, error) {
371411
// Data are ready on the wire, no need to reinitiate connection in lower level
372412
o.seekData = false
373413

414+
if response.ObjectSize != 0 {
415+
o.totalSize = response.ObjectSize
416+
}
417+
374418
return response, response.Error
375419
}
376420

@@ -380,7 +424,7 @@ func (o *Object) setOffset(bytesRead int64) error {
380424
// Update the currentOffset.
381425
o.currOffset += bytesRead
382426

383-
if o.objectInfo.Size > -1 && o.currOffset >= o.objectInfo.Size {
427+
if o.totalSize > -1 && o.currOffset >= o.totalSize {
384428
return io.EOF
385429
}
386430
return nil
@@ -458,6 +502,8 @@ func (o *Object) Read(b []byte) (n int, err error) {
458502
}
459503

460504
// Stat returns the ObjectInfo structure describing Object.
505+
// When requesting a partial object or reading has started,
506+
// the size returned will reflect the remaining size.
461507
func (o *Object) Stat() (ObjectInfo, error) {
462508
if o == nil {
463509
return ObjectInfo{}, errInvalidArgument("Object is nil")
@@ -474,6 +520,12 @@ func (o *Object) Stat() (ObjectInfo, error) {
474520
// payload is delivered out-of-band so the object is created closed, or a
475521
// previously completed request) report it, even for a closed object.
476522
if o.objectInfoSet {
523+
if o.currOffset > o.totalSize {
524+
return ObjectInfo{}, io.EOF
525+
}
526+
if o.currOffset <= o.totalSize {
527+
o.objectInfo.Size = o.totalSize - o.currOffset
528+
}
477529
return o.objectInfo, nil
478530
}
479531

@@ -493,7 +545,12 @@ func (o *Object) Stat() (ObjectInfo, error) {
493545
return ObjectInfo{}, err
494546
}
495547
}
496-
548+
if o.currOffset > o.totalSize {
549+
return ObjectInfo{}, io.EOF
550+
}
551+
if o.currOffset <= o.totalSize {
552+
o.objectInfo.Size = o.totalSize - o.currOffset
553+
}
497554
return o.objectInfo, nil
498555
}
499556

@@ -529,7 +586,7 @@ func (o *Object) ReadAt(b []byte, offset int64) (n int, err error) {
529586
if o.objectInfoSet {
530587
// If offset is negative than we return io.EOF.
531588
// If offset is greater than or equal to object size we return io.EOF.
532-
if (o.objectInfo.Size > -1 && offset >= o.objectInfo.Size) || offset < 0 {
589+
if (o.totalSize > -1 && offset >= o.totalSize) || offset < 0 {
533590
return 0, io.EOF
534591
}
535592
}
@@ -643,10 +700,10 @@ func (o *Object) Seek(offset int64, whence int) (n int64, err error) {
643700
newOffset += offset
644701
case 2:
645702
// If we don't know the object size return an error for io.SeekEnd
646-
if o.objectInfo.Size < 0 {
703+
if o.totalSize < 0 {
647704
return 0, errInvalidArgument("Whence END is not supported when the object size is unknown")
648705
}
649-
newOffset = o.objectInfo.Size + offset
706+
newOffset = o.totalSize + offset
650707
}
651708
// Seeking to a position before the start of the object is not allowed for
652709
// any whence.

api-get-object_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ func TestObjectSeekAtObjectSizeAllowsSubsequentReadEOF(t *testing.T) {
383383
objectInfo: ObjectInfo{Size: 10},
384384
objectInfoSet: true,
385385
isStarted: true,
386+
totalSize: 10,
386387
}
387388

388389
n, err := o.Seek(10, io.SeekStart)
@@ -429,6 +430,7 @@ func TestObjectSeekPastObjectSizeReturnsEOF(t *testing.T) {
429430
objectInfo: ObjectInfo{Size: 10},
430431
objectInfoSet: true,
431432
isStarted: true,
433+
totalSize: 10,
432434
}
433435

434436
n, err := o.Seek(11, io.SeekStart)
@@ -517,6 +519,7 @@ func TestObjectSeekEndUnknownSize(t *testing.T) {
517519
objectInfo: ObjectInfo{Size: -1},
518520
objectInfoSet: true,
519521
isStarted: true,
522+
totalSize: -1,
520523
}
521524

522525
if _, err := o.Seek(0, io.SeekEnd); err == nil {

0 commit comments

Comments
 (0)