From 607aafc59f4b721537f87f1d6856ff5a94f0cf28 Mon Sep 17 00:00:00 2001 From: xelat09 Date: Sun, 12 Jan 2025 21:00:30 +0100 Subject: [PATCH 1/8] implement ContentServerDataStore for azure blob storage too, support more headers and Range without rangeEnd --- pkg/azurestore/azureservice.go | 73 ++++++++++++++++++++++++++++++++++ pkg/azurestore/azurestore.go | 11 +++++ 2 files changed, 84 insertions(+) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index 4bac8d0af..54e62c50d 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io" + "net/http" "sort" "strings" @@ -67,6 +68,8 @@ type AzBlob interface { Upload(ctx context.Context, body io.ReadSeeker) error // Download returns a readcloser to download the contents of the blob Download(ctx context.Context) (io.ReadCloser, error) + // Serves the contents of the blob directly handling special HTTP headers like Range, if set + ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error // Get the offset of the blob and its indexes GetOffset(ctx context.Context) (int64, error) // Commit the uploaded blocks to the BlockBlob @@ -187,6 +190,31 @@ func (blockBlob *BlockBlob) Download(ctx context.Context) (io.ReadCloser, error) return resp.Body, nil } +// Serve content respecting range header +func (blockBlob *BlockBlob) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var downloadOptions, err = parseDownloadOptions(r) + if err != nil { + return err + } + resp, err := blockBlob.BlobClient.DownloadStream(ctx, downloadOptions) + if err != nil { + return err + } + + statusCode := http.StatusOK + if resp.ContentRange != nil { + // Use 206 Partial Content for range requests + statusCode = http.StatusPartialContent + } else if resp.ContentLength != nil && *resp.ContentLength == 0 { + statusCode = http.StatusNoContent + } + w.WriteHeader(statusCode) + + _, err = io.Copy(w, resp.Body) + resp.Body.Close() + return err +} + func (blockBlob *BlockBlob) GetOffset(ctx context.Context) (int64, error) { // Get the offset of the file from azure storage // For the blob, show each block (ID and size) that is a committed part of it. @@ -253,6 +281,11 @@ func (infoBlob *InfoBlob) Download(ctx context.Context) (io.ReadCloser, error) { return resp.Body, nil } +// ServeContent is not needed for infoBlob +func (infoBlob *InfoBlob) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + return nil +} + // infoBlob does not utilise offset, so just return 0, nil func (infoBlob *InfoBlob) GetOffset(ctx context.Context) (int64, error) { return 0, nil @@ -309,3 +342,43 @@ func checkForNotFoundError(err error) error { } return err } + +// parse the Range, If-Match, If-None-Match, If-Unmodified-Since, If-Modified-Since headers if present +func parseDownloadOptions(r *http.Request) (*azblob.DownloadStreamOptions, error) { + input := azblob.DownloadStreamOptions{AccessConditions: &azblob.AccessConditions{}} + + if val := r.Header.Get("Range"); val != "" { + // zero value count indicates from the offset to the resource's end, suffix-length is not required + input.Range = azblob.HTTPRange{Offset: 0, Count: 0} + if _, err := fmt.Sscanf(val, "bytes=%d-%d", &input.Range.Offset, &input.Range.Count); err != nil { + if _, err := fmt.Sscanf(val, "bytes=%d-", &input.Range.Offset); err != nil { + return nil, err + } + } + } + if val := r.Header.Get("If-Match"); val != "" { + etagIfMatch := azcore.ETag(val) + input.AccessConditions.ModifiedAccessConditions.IfMatch = &etagIfMatch + } + if val := r.Header.Get("If-None-Match"); val != "" { + etagIfNoneMatch := azcore.ETag(val) + input.AccessConditions.ModifiedAccessConditions.IfNoneMatch = &etagIfNoneMatch + } + if val := r.Header.Get("If-Modified-Since"); val != "" { + t, err := http.ParseTime(val) + if err != nil { + return nil, err + } + input.AccessConditions.ModifiedAccessConditions.IfModifiedSince = &t + + } + if val := r.Header.Get("If-Unmodified-Since"); val != "" { + t, err := http.ParseTime(val) + if err != nil { + return nil, err + } + input.AccessConditions.ModifiedAccessConditions.IfUnmodifiedSince = &t + } + + return &input, nil +} diff --git a/pkg/azurestore/azurestore.go b/pkg/azurestore/azurestore.go index 0768bb421..5951e8dd4 100644 --- a/pkg/azurestore/azurestore.go +++ b/pkg/azurestore/azurestore.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "net/http" "io/fs" "os" "strings" @@ -47,6 +48,7 @@ func (store AzureStore) UseIn(composer *handler.StoreComposer) { composer.UseCore(store) composer.UseTerminater(store) composer.UseLengthDeferrer(store) + composer.UseContentServer(store) } func (store AzureStore) NewUpload(ctx context.Context, info handler.FileInfo) (handler.Upload, error) { @@ -149,6 +151,10 @@ func (store AzureStore) AsLengthDeclarableUpload(upload handler.Upload) handler. return upload.(*AzUpload) } +func (store AzureStore) AsServableUpload(upload handler.Upload) handler.ServableUpload { + return upload.(*AzUpload) +} + func (upload *AzUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { // Create a temporary file for holding the uploaded data file, err := os.CreateTemp(upload.tempDir, "tusd-az-tmp-") @@ -214,6 +220,11 @@ func (upload *AzUpload) GetReader(ctx context.Context) (io.ReadCloser, error) { return upload.BlockBlob.Download(ctx) } +// Serves the contents of the blob directly handling special HTTP headers like Range, if set +func (upload *AzUpload) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + return upload.BlockBlob.ServeContent(ctx, w, r) +} + // Finish the file upload and commit the block list func (upload *AzUpload) FinishUpload(ctx context.Context) error { return upload.BlockBlob.Commit(ctx) From c0f70f3f415b15cb1faef9c8615aa89773192e50 Mon Sep 17 00:00:00 2001 From: xelat09 Date: Fri, 17 Jan 2025 20:12:15 +0100 Subject: [PATCH 2/8] format source code --- pkg/azurestore/azureservice.go | 120 ++++++++++++++++----------------- pkg/azurestore/azurestore.go | 2 +- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index 54e62c50d..c6dd8b7e5 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -68,8 +68,8 @@ type AzBlob interface { Upload(ctx context.Context, body io.ReadSeeker) error // Download returns a readcloser to download the contents of the blob Download(ctx context.Context) (io.ReadCloser, error) - // Serves the contents of the blob directly handling special HTTP headers like Range, if set - ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error + // Serves the contents of the blob directly handling special HTTP headers like Range, if set + ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error // Get the offset of the blob and its indexes GetOffset(ctx context.Context) (int64, error) // Commit the uploaded blocks to the BlockBlob @@ -192,27 +192,27 @@ func (blockBlob *BlockBlob) Download(ctx context.Context) (io.ReadCloser, error) // Serve content respecting range header func (blockBlob *BlockBlob) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - var downloadOptions, err = parseDownloadOptions(r) - if err != nil { - return err - } - resp, err := blockBlob.BlobClient.DownloadStream(ctx, downloadOptions) - if err != nil { - return err - } - - statusCode := http.StatusOK - if resp.ContentRange != nil { - // Use 206 Partial Content for range requests - statusCode = http.StatusPartialContent - } else if resp.ContentLength != nil && *resp.ContentLength == 0 { - statusCode = http.StatusNoContent - } - w.WriteHeader(statusCode) - - _, err = io.Copy(w, resp.Body) - resp.Body.Close() - return err + var downloadOptions, err = parseDownloadOptions(r) + if err != nil { + return err + } + resp, err := blockBlob.BlobClient.DownloadStream(ctx, downloadOptions) + if err != nil { + return err + } + + statusCode := http.StatusOK + if resp.ContentRange != nil { + // Use 206 Partial Content for range requests + statusCode = http.StatusPartialContent + } else if resp.ContentLength != nil && *resp.ContentLength == 0 { + statusCode = http.StatusNoContent + } + w.WriteHeader(statusCode) + + _, err = io.Copy(w, resp.Body) + resp.Body.Close() + return err } func (blockBlob *BlockBlob) GetOffset(ctx context.Context) (int64, error) { @@ -283,7 +283,7 @@ func (infoBlob *InfoBlob) Download(ctx context.Context) (io.ReadCloser, error) { // ServeContent is not needed for infoBlob func (infoBlob *InfoBlob) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - return nil + return nil } // infoBlob does not utilise offset, so just return 0, nil @@ -345,40 +345,40 @@ func checkForNotFoundError(err error) error { // parse the Range, If-Match, If-None-Match, If-Unmodified-Since, If-Modified-Since headers if present func parseDownloadOptions(r *http.Request) (*azblob.DownloadStreamOptions, error) { - input := azblob.DownloadStreamOptions{AccessConditions: &azblob.AccessConditions{}} - - if val := r.Header.Get("Range"); val != "" { - // zero value count indicates from the offset to the resource's end, suffix-length is not required - input.Range = azblob.HTTPRange{Offset: 0, Count: 0} - if _, err := fmt.Sscanf(val, "bytes=%d-%d", &input.Range.Offset, &input.Range.Count); err != nil { - if _, err := fmt.Sscanf(val, "bytes=%d-", &input.Range.Offset); err != nil { - return nil, err - } - } - } - if val := r.Header.Get("If-Match"); val != "" { - etagIfMatch := azcore.ETag(val) - input.AccessConditions.ModifiedAccessConditions.IfMatch = &etagIfMatch - } - if val := r.Header.Get("If-None-Match"); val != "" { - etagIfNoneMatch := azcore.ETag(val) - input.AccessConditions.ModifiedAccessConditions.IfNoneMatch = &etagIfNoneMatch - } - if val := r.Header.Get("If-Modified-Since"); val != "" { - t, err := http.ParseTime(val) - if err != nil { - return nil, err - } - input.AccessConditions.ModifiedAccessConditions.IfModifiedSince = &t - - } - if val := r.Header.Get("If-Unmodified-Since"); val != "" { - t, err := http.ParseTime(val) - if err != nil { - return nil, err - } - input.AccessConditions.ModifiedAccessConditions.IfUnmodifiedSince = &t - } - - return &input, nil + input := azblob.DownloadStreamOptions{AccessConditions: &azblob.AccessConditions{}} + + if val := r.Header.Get("Range"); val != "" { + // zero value count indicates from the offset to the resource's end, suffix-length is not required + input.Range = azblob.HTTPRange{Offset: 0, Count: 0} + if _, err := fmt.Sscanf(val, "bytes=%d-%d", &input.Range.Offset, &input.Range.Count); err != nil { + if _, err := fmt.Sscanf(val, "bytes=%d-", &input.Range.Offset); err != nil { + return nil, err + } + } + } + if val := r.Header.Get("If-Match"); val != "" { + etagIfMatch := azcore.ETag(val) + input.AccessConditions.ModifiedAccessConditions.IfMatch = &etagIfMatch + } + if val := r.Header.Get("If-None-Match"); val != "" { + etagIfNoneMatch := azcore.ETag(val) + input.AccessConditions.ModifiedAccessConditions.IfNoneMatch = &etagIfNoneMatch + } + if val := r.Header.Get("If-Modified-Since"); val != "" { + t, err := http.ParseTime(val) + if err != nil { + return nil, err + } + input.AccessConditions.ModifiedAccessConditions.IfModifiedSince = &t + + } + if val := r.Header.Get("If-Unmodified-Since"); val != "" { + t, err := http.ParseTime(val) + if err != nil { + return nil, err + } + input.AccessConditions.ModifiedAccessConditions.IfUnmodifiedSince = &t + } + + return &input, nil } diff --git a/pkg/azurestore/azurestore.go b/pkg/azurestore/azurestore.go index 5951e8dd4..dc85d9f2a 100644 --- a/pkg/azurestore/azurestore.go +++ b/pkg/azurestore/azurestore.go @@ -7,8 +7,8 @@ import ( "errors" "fmt" "io" - "net/http" "io/fs" + "net/http" "os" "strings" From e7d6c402e3d763e72db854649ca5f59376328c77 Mon Sep 17 00:00:00 2001 From: xelat09 Date: Fri, 17 Jan 2025 20:14:45 +0100 Subject: [PATCH 3/8] return an error here so we notice it early if this method is accidentally called. --- pkg/azurestore/azureservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index c6dd8b7e5..c588339cd 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -283,7 +283,7 @@ func (infoBlob *InfoBlob) Download(ctx context.Context) (io.ReadCloser, error) { // ServeContent is not needed for infoBlob func (infoBlob *InfoBlob) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - return nil + return errors.New("azurestore: ServeContent is not implemented for InfoBlob") } // infoBlob does not utilise offset, so just return 0, nil From 922e53bb29dad5c917854cb962656eccde33497e Mon Sep 17 00:00:00 2001 From: xelat09 Date: Fri, 17 Jan 2025 21:35:01 +0100 Subject: [PATCH 4/8] forward response headers --- pkg/azurestore/azureservice.go | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index c588339cd..ca35ef06f 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -23,6 +23,7 @@ import ( "io" "net/http" "sort" + "strconv" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -196,22 +197,55 @@ func (blockBlob *BlockBlob) ServeContent(ctx context.Context, w http.ResponseWri if err != nil { return err } - resp, err := blockBlob.BlobClient.DownloadStream(ctx, downloadOptions) + result, err := blockBlob.BlobClient.DownloadStream(ctx, downloadOptions) if err != nil { return err } + defer result.Body.Close() statusCode := http.StatusOK - if resp.ContentRange != nil { + if result.ContentRange != nil { // Use 206 Partial Content for range requests statusCode = http.StatusPartialContent - } else if resp.ContentLength != nil && *resp.ContentLength == 0 { + } else if result.ContentLength != nil && *result.ContentLength == 0 { statusCode = http.StatusNoContent } + + // Add Accept-Ranges,Content-*, Cache-Control, ETag, Expires, Last-Modified headers if present in S3 response + if result.AcceptRanges != nil { + w.Header().Set("Accept-Ranges", *result.AcceptRanges) + } + if result.ContentDisposition != nil { + w.Header().Set("Content-Disposition", *result.ContentDisposition) + } + if result.ContentEncoding != nil { + w.Header().Set("Content-Encoding", *result.ContentEncoding) + } + if result.ContentLanguage != nil { + w.Header().Set("Content-Language", *result.ContentLanguage) + } + if result.ContentLength != nil { + w.Header().Set("Content-Length", strconv.FormatInt(*result.ContentLength, 10)) + } + if result.ContentRange != nil { + w.Header().Set("Content-Range", *result.ContentRange) + } + if result.ContentType != nil { + w.Header().Set("Content-Type", *result.ContentType) + } + if result.CacheControl != nil { + w.Header().Set("Cache-Control", *result.CacheControl) + } + if result.ETag != nil && *result.ETag != "" { + w.Header().Set("ETag", string(*result.ETag)) + } + if result.LastModified != nil { + w.Header().Set("Last-Modified", result.LastModified.Format(http.TimeFormat)) + } + w.WriteHeader(statusCode) - _, err = io.Copy(w, resp.Body) - resp.Body.Close() + _, err = io.Copy(w, result.Body) return err } From 874f79ef76255689f9def261cafdb5a44de3418c Mon Sep 17 00:00:00 2001 From: xelat09 Date: Sun, 19 Jan 2025 14:08:28 +0100 Subject: [PATCH 5/8] fix test --- pkg/azurestore/azurestore_mock_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/azurestore/azurestore_mock_test.go b/pkg/azurestore/azurestore_mock_test.go index 48000a9ca..ac10bfc51 100644 --- a/pkg/azurestore/azurestore_mock_test.go +++ b/pkg/azurestore/azurestore_mock_test.go @@ -7,6 +7,7 @@ package azurestore_test import ( context "context" io "io" + http "net/http" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -117,6 +118,20 @@ func (mr *MockAzBlobMockRecorder) Download(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockAzBlob)(nil).Download), arg0) } +// Download mocks base method. +func (m *MockAzBlob) ServeContent(arg0 context.Context, w http.ResponseWriter, r *http.Request) (error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServeContent", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Download indicates an expected call of Download. +func (mr *MockAzBlobMockRecorder) ServeContent(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServeContent", reflect.TypeOf((*MockAzBlob)(nil).ServeContent), arg0) +} + // GetOffset mocks base method. func (m *MockAzBlob) GetOffset(arg0 context.Context) (int64, error) { m.ctrl.T.Helper() From 7607b9394018934d64b954be9731ab117665b344 Mon Sep 17 00:00:00 2001 From: xelat09 Date: Mon, 20 Jan 2025 20:52:30 +0100 Subject: [PATCH 6/8] fix test by rerunning mockgen --- pkg/azurestore/azureservice.go | 4 ++-- pkg/azurestore/azurestore_mock_test.go | 28 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index ca35ef06f..ec6e3141b 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -193,7 +193,7 @@ func (blockBlob *BlockBlob) Download(ctx context.Context) (io.ReadCloser, error) // Serve content respecting range header func (blockBlob *BlockBlob) ServeContent(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - var downloadOptions, err = parseDownloadOptions(r) + var downloadOptions, err = ParseDownloadOptions(r) if err != nil { return err } @@ -378,7 +378,7 @@ func checkForNotFoundError(err error) error { } // parse the Range, If-Match, If-None-Match, If-Unmodified-Since, If-Modified-Since headers if present -func parseDownloadOptions(r *http.Request) (*azblob.DownloadStreamOptions, error) { +func ParseDownloadOptions(r *http.Request) (*azblob.DownloadStreamOptions, error) { input := azblob.DownloadStreamOptions{AccessConditions: &azblob.AccessConditions{}} if val := r.Header.Get("Range"); val != "" { diff --git a/pkg/azurestore/azurestore_mock_test.go b/pkg/azurestore/azurestore_mock_test.go index ac10bfc51..6ae735165 100644 --- a/pkg/azurestore/azurestore_mock_test.go +++ b/pkg/azurestore/azurestore_mock_test.go @@ -118,20 +118,6 @@ func (mr *MockAzBlobMockRecorder) Download(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockAzBlob)(nil).Download), arg0) } -// Download mocks base method. -func (m *MockAzBlob) ServeContent(arg0 context.Context, w http.ResponseWriter, r *http.Request) (error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ServeContent", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Download indicates an expected call of Download. -func (mr *MockAzBlobMockRecorder) ServeContent(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServeContent", reflect.TypeOf((*MockAzBlob)(nil).ServeContent), arg0) -} - // GetOffset mocks base method. func (m *MockAzBlob) GetOffset(arg0 context.Context) (int64, error) { m.ctrl.T.Helper() @@ -147,6 +133,20 @@ func (mr *MockAzBlobMockRecorder) GetOffset(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOffset", reflect.TypeOf((*MockAzBlob)(nil).GetOffset), arg0) } +// ServeContent mocks base method. +func (m *MockAzBlob) ServeContent(arg0 context.Context, arg1 http.ResponseWriter, arg2 *http.Request) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServeContent", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ServeContent indicates an expected call of ServeContent. +func (mr *MockAzBlobMockRecorder) ServeContent(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServeContent", reflect.TypeOf((*MockAzBlob)(nil).ServeContent), arg0, arg1, arg2) +} + // Upload mocks base method. func (m *MockAzBlob) Upload(arg0 context.Context, arg1 io.ReadSeeker) error { m.ctrl.T.Helper() From 5a7b4970c13a5726120dcafc7dea578d1a6c51ce Mon Sep 17 00:00:00 2001 From: xelat09 Date: Tue, 21 Jan 2025 01:20:09 +0100 Subject: [PATCH 7/8] a few tests + fix, not sure if this is good enough.. --- pkg/azurestore/azureservice.go | 6 +- pkg/azurestore/azurestore_test.go | 127 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index ec6e3141b..a9f98e94e 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -384,11 +384,15 @@ func ParseDownloadOptions(r *http.Request) (*azblob.DownloadStreamOptions, error if val := r.Header.Get("Range"); val != "" { // zero value count indicates from the offset to the resource's end, suffix-length is not required input.Range = azblob.HTTPRange{Offset: 0, Count: 0} - if _, err := fmt.Sscanf(val, "bytes=%d-%d", &input.Range.Offset, &input.Range.Count); err != nil { + bytesEnd := 0 + if _, err := fmt.Sscanf(val, "bytes=%d-%d", &input.Range.Offset, &bytesEnd); err != nil { if _, err := fmt.Sscanf(val, "bytes=%d-", &input.Range.Offset); err != nil { return nil, err } } + if bytesEnd != 0 { + input.Range.Count = int64(bytesEnd) - input.Range.Offset + 1 + } } if val := r.Header.Get("If-Match"); val != "" { etagIfMatch := azcore.ETag(val) diff --git a/pkg/azurestore/azurestore_test.go b/pkg/azurestore/azurestore_test.go index 5b64e7290..a866357a7 100644 --- a/pkg/azurestore/azurestore_test.go +++ b/pkg/azurestore/azurestore_test.go @@ -6,8 +6,11 @@ import ( "encoding/json" "errors" "io" + "net/http" + "net/http/httptest" "testing" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -431,6 +434,130 @@ func TestDeclareLength(t *testing.T) { cancel() } +func TestAzureStoreAsServerDataStore(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + assert := assert.New(t) + + service := NewMockAzService(mockCtrl) + store := azurestore.New(service) + + mockUpload := &azurestore.AzUpload{} + servableUpload := store.AsServableUpload(mockUpload) + + assert.NotNil(servableUpload) + assert.IsType(&azurestore.AzUpload{}, servableUpload) +} + +func TestAZServableUploadServeContent(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + assert := assert.New(t) + ctx := context.Background() + + blockBlob := NewMockAzBlob(mockCtrl) + assert.NotNil(blockBlob) + + // Create a test HTTP request and response recorder + req := httptest.NewRequest("GET", "/", nil) + rec := httptest.NewRecorder() + + // Expected response headers and body + expectedHeaders := map[string]string{ + "Content-Type": "text/plain", + "Content-Length": "12", + "ETag": "bytes", + "CacheControl": "max-age=3600", + } + expectedBody := "test content" + + // Mock ServeContent call + blockBlob.EXPECT().ServeContent(ctx, gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + // Add headers to response + for key, value := range expectedHeaders { + w.Header().Set(key, value) + } + w.WriteHeader(http.StatusOK) + + // Write response body + _, err := w.Write([]byte(expectedBody)) + return err + }, + ).Times(1) + + err := blockBlob.ServeContent(ctx, rec, req) + + assert.Nil(err) + assert.Equal(http.StatusOK, rec.Code) + for key, value := range expectedHeaders { + assert.Equal(value, rec.Header().Get(key)) + } + assert.Equal(expectedBody, rec.Body.String()) +} + +func TestParseDownloadOptions(t *testing.T) { + tests := []struct { + name string + headers map[string]string + expected *azblob.DownloadStreamOptions + expectErr bool + }{ + { + name: "Valid Range header", + headers: map[string]string{ + "Range": "bytes=10-20", + }, + expected: &azblob.DownloadStreamOptions{ + Range: azblob.HTTPRange{ + Offset: 10, + Count: 11, + }, + }, + expectErr: false, + }, + { + name: "Valid Range header", + headers: map[string]string{ + "Range": "bytes=10-", + }, + expected: &azblob.DownloadStreamOptions{ + Range: azblob.HTTPRange{ + Offset: 10, + Count: 0, + }, + }, + expectErr: false, + }, + { + name: "Valid Range header", + headers: map[string]string{ + "Range": "bytes=zZ-", + }, + expected: &azblob.DownloadStreamOptions{}, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + for key, value := range tt.headers { + req.Header.Set(key, value) + } + + options, err := azurestore.ParseDownloadOptions(req) + if tt.expectErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + options.AccessConditions = nil + assert.Equal(t, tt.expected, options) + } + }) + } +} + func newReadCloser(b []byte) io.ReadCloser { return io.NopCloser(bytes.NewReader(b)) } From ea551c29e5ba8705159c067c241980311beb6d60 Mon Sep 17 00:00:00 2001 From: xelat09 Date: Mon, 2 Jun 2025 22:27:22 +0200 Subject: [PATCH 8/8] just fixed docu --- pkg/azurestore/azureservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index 97b7432a3..38c56bd42 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -223,7 +223,7 @@ func (blockBlob *BlockBlob) ServeContent(ctx context.Context, w http.ResponseWri statusCode = http.StatusNoContent } - // Add Accept-Ranges,Content-*, Cache-Control, ETag, Expires, Last-Modified headers if present in S3 response + // Add Accept-Ranges,Content-*, Cache-Control, ETag, Expires, Last-Modified headers if present in azure response if result.AcceptRanges != nil { w.Header().Set("Accept-Ranges", *result.AcceptRanges) }