@@ -31,6 +31,11 @@ type StorageClient interface {
3131 dest string ,
3232 ) ([]byte , error )
3333
34+ UploadStream (
35+ source io.ReadSeekCloser ,
36+ dest string ,
37+ ) error
38+
3439 Download (
3540 source string ,
3641 dest * os.File ,
@@ -68,6 +73,36 @@ type StorageClient interface {
6873 EnsureContainerExists () error
6974}
7075
76+ // 4 MB of block size
77+ const blockSize = int64 (4 * 1024 * 1024 )
78+
79+ // number of go routines
80+ const maxConcurrency = 5
81+
82+ func createContext (dsc DefaultStorageClient ) (context.Context , context.CancelFunc , error ) {
83+ var ctx context.Context
84+ var cancel context.CancelFunc
85+
86+ if dsc .storageConfig .Timeout != "" {
87+ timeoutInt , err := strconv .Atoi (dsc .storageConfig .Timeout )
88+ timeout := time .Duration (timeoutInt ) * time .Second
89+ if timeout < 1 && err == nil {
90+ slog .Info ("Invalid time, need at least 1 second" , "timeout" , dsc .storageConfig .Timeout )
91+ return nil , nil , fmt .Errorf ("invalid time: %w" , err )
92+ }
93+ if err != nil {
94+ slog .Info ("Invalid timeout format, need seconds as number e.g. 30s" , "timeout" , dsc .storageConfig .Timeout )
95+ return nil , nil , fmt .Errorf ("invalid timeout format: %w" , err )
96+ }
97+ ctx , cancel = context .WithTimeout (context .Background (), timeout )
98+ } else {
99+ ctx , cancel = context .WithCancel (context .Background ())
100+ }
101+
102+ return ctx , cancel , nil
103+
104+ }
105+
71106type DefaultStorageClient struct {
72107 credential * azblob.SharedKeyCredential
73108 serviceURL string
@@ -91,33 +126,23 @@ func (dsc DefaultStorageClient) Upload(
91126) ([]byte , error ) {
92127 blobURL := fmt .Sprintf ("%s/%s" , dsc .serviceURL , dest )
93128
94- var ctx context.Context
95- var cancel context.CancelFunc
96-
97129 if dsc .storageConfig .Timeout != "" {
98- timeoutInt , err := strconv .Atoi (dsc .storageConfig .Timeout )
99- timeout := time .Duration (timeoutInt ) * time .Second
100- if timeout < 1 && err == nil {
101- slog .Info ("Invalid time, need at least 1 second" , "timeout" , dsc .storageConfig .Timeout )
102- return nil , fmt .Errorf ("invalid time: %w" , err )
103- }
104- if err != nil {
105- slog .Info ("Invalid timeout format, need seconds as number e.g. 30s" , "timeout" , dsc .storageConfig .Timeout )
106- return nil , fmt .Errorf ("invalid timeout format: %w" , err )
107- }
108- slog .Info ("Uploading blob to container" , "container" , dsc .storageConfig .ContainerName , "blob" , dest , "url" , blobURL , "timeout" , timeout .String ())
109-
110- ctx , cancel = context .WithTimeout (context .Background (), timeout )
130+ slog .Info ("Uploading blob to container" , "container" , dsc .storageConfig .ContainerName , "blob" , dest , "url" , blobURL , "timeout" , dsc .storageConfig .Timeout )
111131 } else {
112132 slog .Info ("Uploading blob to container" , "container" , dsc .storageConfig .ContainerName , "blob" , dest , "url" , blobURL )
113- ctx , cancel = context .WithCancel (context .Background ())
133+ }
134+
135+ ctx , cancel , err := createContext (dsc )
136+ if err != nil {
137+ return nil , err
114138 }
115139 defer cancel ()
116140
117141 client , err := blockblob .NewClientWithSharedKeyCredential (blobURL , dsc .credential , nil )
118142 if err != nil {
119143 return nil , err
120144 }
145+
121146 uploadResponse , err := client .Upload (ctx , source , nil )
122147 if err != nil {
123148 if dsc .storageConfig .Timeout != "" && errors .Is (err , context .DeadlineExceeded ) {
@@ -127,7 +152,42 @@ func (dsc DefaultStorageClient) Upload(
127152 }
128153
129154 slog .Info ("Successfully uploaded blob" , "container" , dsc .storageConfig .ContainerName , "blob" , dest )
130- return uploadResponse .ContentMD5 , err
155+ return uploadResponse .ContentMD5 , nil
156+ }
157+
158+ func (dsc DefaultStorageClient ) UploadStream (
159+ source io.ReadSeekCloser ,
160+ dest string ,
161+ ) error {
162+ blobURL := fmt .Sprintf ("%s/%s" , dsc .serviceURL , dest )
163+
164+ if dsc .storageConfig .Timeout != "" {
165+ slog .Info ("UploadStreaming blob to container" , "container" , dsc .storageConfig .ContainerName , "blob" , dest , "url" , blobURL , "timeout" , dsc .storageConfig .Timeout )
166+ } else {
167+ slog .Info ("UploadStreaming blob to container" , "container" , dsc .storageConfig .ContainerName , "blob" , dest , "url" , blobURL )
168+ }
169+
170+ ctx , cancel , err := createContext (dsc )
171+ if err != nil {
172+ return err
173+ }
174+ defer cancel ()
175+
176+ client , err := blockblob .NewClientWithSharedKeyCredential (blobURL , dsc .credential , nil )
177+ if err != nil {
178+ return err
179+ }
180+
181+ _ , err = client .UploadStream (ctx , source , & azblob.UploadStreamOptions {BlockSize : blockSize , Concurrency : maxConcurrency })
182+ if err != nil {
183+ if dsc .storageConfig .Timeout != "" && errors .Is (err , context .DeadlineExceeded ) {
184+ return fmt .Errorf ("upload failed: timeout of %s reached while uploading %s" , dsc .storageConfig .Timeout , dest )
185+ }
186+ return fmt .Errorf ("upload failure: %w" , err )
187+ }
188+
189+ slog .Info ("Successfully uploaded blob" , "container" , dsc .storageConfig .ContainerName , "blob" , dest )
190+ return nil
131191}
132192
133193func (dsc DefaultStorageClient ) Download (
0 commit comments