@@ -21,7 +21,9 @@ import (
21
21
"errors"
22
22
"fmt"
23
23
"io"
24
+ "net/http"
24
25
"sort"
26
+ "strconv"
25
27
"strings"
26
28
27
29
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -67,6 +69,8 @@ type AzBlob interface {
67
69
Upload (ctx context.Context , body io.ReadSeeker ) error
68
70
// Download returns a readcloser to download the contents of the blob
69
71
Download (ctx context.Context ) (io.ReadCloser , error )
72
+ // Serves the contents of the blob directly handling HTTP Range requests if set
73
+ ServeContent (ctx context.Context , w http.ResponseWriter , r * http.Request ) error
70
74
// Get the offset of the blob and its indexes
71
75
GetOffset (ctx context.Context ) (int64 , error )
72
76
// Commit the uploaded blocks to the BlockBlob
@@ -187,6 +191,31 @@ func (blockBlob *BlockBlob) Download(ctx context.Context) (io.ReadCloser, error)
187
191
return resp .Body , nil
188
192
}
189
193
194
+ // Serve content respecting range header
195
+ func (blockBlob * BlockBlob ) ServeContent (ctx context.Context , w http.ResponseWriter , r * http.Request ) error {
196
+ var downloadOptions , err = parseHTTPRange (r )
197
+ if err != nil {
198
+ return err
199
+ }
200
+ resp , err := blockBlob .BlobClient .DownloadStream (ctx , downloadOptions )
201
+ if err != nil {
202
+ return err
203
+ }
204
+
205
+ statusCode := http .StatusOK
206
+ if resp .ContentRange != nil {
207
+ // Use 206 Partial Content for range requests
208
+ statusCode = http .StatusPartialContent
209
+ } else if resp .ContentLength != nil && * resp .ContentLength == 0 {
210
+ statusCode = http .StatusNoContent
211
+ }
212
+ w .WriteHeader (statusCode )
213
+
214
+ _ , err = io .Copy (w , resp .Body )
215
+ resp .Body .Close ()
216
+ return err
217
+ }
218
+
190
219
func (blockBlob * BlockBlob ) GetOffset (ctx context.Context ) (int64 , error ) {
191
220
// Get the offset of the file from azure storage
192
221
// For the blob, show each block (ID and size) that is a committed part of it.
@@ -253,6 +282,11 @@ func (infoBlob *InfoBlob) Download(ctx context.Context) (io.ReadCloser, error) {
253
282
return resp .Body , nil
254
283
}
255
284
285
+ // ServeContent is not needed for infoBlob
286
+ func (infoBlob * InfoBlob ) ServeContent (ctx context.Context , w http.ResponseWriter , r * http.Request ) error {
287
+ return nil
288
+ }
289
+
256
290
// infoBlob does not utilise offset, so just return 0, nil
257
291
func (infoBlob * InfoBlob ) GetOffset (ctx context.Context ) (int64 , error ) {
258
292
return 0 , nil
@@ -309,3 +343,37 @@ func checkForNotFoundError(err error) error {
309
343
}
310
344
return err
311
345
}
346
+
347
+ // simple parse http ranging, no multipart ranges/no if-range/no last-modified, not supported by azure anyway
348
+ func parseHTTPRange (r * http.Request ) (* azblob.DownloadStreamOptions , error ) {
349
+ rangeHeader := r .Header .Get ("Range" )
350
+ if rangeHeader == "" {
351
+ // this is totally fine, Range header is not required
352
+ return nil , nil
353
+ }
354
+
355
+ const prefix = "bytes="
356
+ if ! strings .HasPrefix (rangeHeader , prefix ) {
357
+ return nil , fmt .Errorf ("invalid Range header format" )
358
+ }
359
+
360
+ rangeParts := strings .Split (strings .TrimPrefix (rangeHeader , prefix ), "-" )
361
+ if len (rangeParts ) != 2 {
362
+ return nil , fmt .Errorf ("invalid Range header format" )
363
+ }
364
+
365
+ offset , err := strconv .ParseInt (rangeParts [0 ], 10 , 64 )
366
+ if err != nil {
367
+ return nil , fmt .Errorf ("invalid offset in Range header" )
368
+ }
369
+
370
+ count , err := strconv .ParseInt (rangeParts [1 ], 10 , 64 )
371
+ if err != nil {
372
+ return nil , fmt .Errorf ("invalid count in Range header" )
373
+ }
374
+
375
+ downloadOptions := azblob.DownloadStreamOptions {}
376
+ downloadOptions .Range .Offset = offset
377
+ downloadOptions .Range .Count = count - offset + 1
378
+ return & downloadOptions , nil
379
+ }
0 commit comments