azure-blob 0.5.8 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/azure_blob/blob_list.rb +1 -1
- data/lib/azure_blob/canonicalized_resource.rb +1 -1
- data/lib/azure_blob/client.rb +44 -23
- data/lib/azure_blob/http.rb +2 -2
- data/lib/azure_blob/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: acd470dcd917d78f24d3a0c6b32f894ea174d2ec774f322a151b64175371ecfd
|
4
|
+
data.tar.gz: dbde6c98095cf2ca14acc2478660f22cdd503c6a84f931fb0935768d4f4a68fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b04b7147d27d49480a3e8c8ffb8e22362bd902be6f57550c2dc79570893d6101016d16c30e4014b6af1d09eeb6b40a7790e17b226776963d8e9b76b06d1d43cf
|
7
|
+
data.tar.gz: '0380bcadb8158e353716dcbc0d276a41a485d9866fa6e0deb0fb34df24654686cf1fe9fb51e58d228fbb39c75f061d5be1ef6895df11cd809f727e050bd49473'
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.9] 2025-05-31
|
4
|
+
|
5
|
+
- Add support for additional headers to all endpoints
|
6
|
+
- Fix typo in class name `AzureBlob::ForbidenError` to `AzureBlob::ForbiddenError`
|
7
|
+
- Fix proper URI encoding for keys with special characters like question marks
|
8
|
+
- Bug fix: Use bytesize to get content size for multi-byte characters
|
9
|
+
|
3
10
|
## [0.5.8] 2025-05-14
|
4
11
|
|
5
12
|
- Add support for copying blobs across containers (#24)
|
data/lib/azure_blob/blob_list.rb
CHANGED
@@ -5,7 +5,7 @@ module AzureBlob
|
|
5
5
|
def initialize(uri, account_name, service_name: nil, url_safe: true)
|
6
6
|
# This next line is needed because CanonicalizedResource
|
7
7
|
# need to be escaped for auhthorization headers, but not SAS tokens
|
8
|
-
path = url_safe ? uri.path : URI::
|
8
|
+
path = url_safe ? uri.path : URI::RFC2396_PARSER.unescape(uri.path)
|
9
9
|
resource = "/#{account_name}#{path.empty? ? "/" : path}"
|
10
10
|
resource = "/#{service_name}#{resource}" if service_name
|
11
11
|
params = CGI.parse(uri.query.to_s)
|
data/lib/azure_blob/client.rb
CHANGED
@@ -10,6 +10,7 @@ require_relative "shared_key_signer"
|
|
10
10
|
require_relative "entra_id_signer"
|
11
11
|
require "time"
|
12
12
|
require "base64"
|
13
|
+
require "stringio"
|
13
14
|
|
14
15
|
module AzureBlob
|
15
16
|
# AzureBlob Client class. You interact with the Azure Blob api
|
@@ -48,7 +49,7 @@ module AzureBlob
|
|
48
49
|
# [+:block_size+]
|
49
50
|
# Block size in bytes, can be used to force the method to split the upload in smaller chunk. Defaults to +AzureBlob::DEFAULT_BLOCK_SIZE+ and cannot be bigger than +AzureBlob::MAX_UPLOAD_SIZE+
|
50
51
|
def create_block_blob(key, content, options = {})
|
51
|
-
if content
|
52
|
+
if content_size(content) > (options[:block_size] || DEFAULT_BLOCK_SIZE)
|
52
53
|
put_blob_multiple(key, content, **options)
|
53
54
|
else
|
54
55
|
put_blob_single(key, content, **options)
|
@@ -72,7 +73,7 @@ module AzureBlob
|
|
72
73
|
|
73
74
|
headers = {
|
74
75
|
"x-ms-range": options[:start] && "bytes=#{options[:start]}-#{options[:end]}",
|
75
|
-
}
|
76
|
+
}.merge(additional_headers(options))
|
76
77
|
|
77
78
|
Http.new(uri, headers, signer:).get
|
78
79
|
end
|
@@ -97,7 +98,7 @@ module AzureBlob
|
|
97
98
|
headers = {
|
98
99
|
"x-ms-copy-source": source_uri.to_s,
|
99
100
|
"x-ms-requires-sync": "true",
|
100
|
-
}
|
101
|
+
}.merge(additional_headers(options))
|
101
102
|
|
102
103
|
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put
|
103
104
|
end
|
@@ -116,7 +117,7 @@ module AzureBlob
|
|
116
117
|
|
117
118
|
headers = {
|
118
119
|
"x-ms-delete-snapshots": options[:delete_snapshots] || "include",
|
119
|
-
}
|
120
|
+
}.merge(additional_headers(options))
|
120
121
|
|
121
122
|
Http.new(uri, headers, signer:).delete
|
122
123
|
end
|
@@ -157,7 +158,7 @@ module AzureBlob
|
|
157
158
|
query[:marker] = marker
|
158
159
|
query.reject! { |key, value| value.to_s.empty? }
|
159
160
|
uri.query = URI.encode_www_form(**query)
|
160
|
-
response = Http.new(uri, signer:).get
|
161
|
+
response = Http.new(uri, additional_headers(options), signer:).get
|
161
162
|
end
|
162
163
|
|
163
164
|
BlobList.new(fetcher)
|
@@ -172,7 +173,7 @@ module AzureBlob
|
|
172
173
|
def get_blob_properties(key, options = {})
|
173
174
|
uri = generate_uri("#{container}/#{key}")
|
174
175
|
|
175
|
-
response = Http.new(uri, signer:).head
|
176
|
+
response = Http.new(uri, additional_headers(options), signer:).head
|
176
177
|
|
177
178
|
Blob.new(response)
|
178
179
|
end
|
@@ -193,9 +194,10 @@ module AzureBlob
|
|
193
194
|
# Takes a key (path) of the blob.
|
194
195
|
#
|
195
196
|
# Returns a hash of the blob's tags.
|
196
|
-
def get_blob_tags(key)
|
197
|
-
uri = generate_uri("#{container}/#{key}
|
198
|
-
|
197
|
+
def get_blob_tags(key, options = {})
|
198
|
+
uri = generate_uri("#{container}/#{key}")
|
199
|
+
uri.query = URI.encode_www_form(comp: "tags")
|
200
|
+
response = Http.new(uri, additional_headers(options), signer:).get
|
199
201
|
|
200
202
|
Tags.from_response(response).to_h
|
201
203
|
end
|
@@ -208,7 +210,7 @@ module AzureBlob
|
|
208
210
|
def get_container_properties(options = {})
|
209
211
|
uri = generate_uri(container)
|
210
212
|
uri.query = URI.encode_www_form(restype: "container")
|
211
|
-
response = Http.new(uri, signer:, raise_on_error: false).head
|
213
|
+
response = Http.new(uri, additional_headers(options), signer:, raise_on_error: false).head
|
212
214
|
|
213
215
|
Container.new(response)
|
214
216
|
end
|
@@ -228,6 +230,7 @@ module AzureBlob
|
|
228
230
|
headers = {}
|
229
231
|
headers[:"x-ms-blob-public-access"] = "blob" if options[:public_access]
|
230
232
|
headers[:"x-ms-blob-public-access"] = options[:public_access] if [ "container", "blob" ].include?(options[:public_access])
|
233
|
+
headers.merge!(additional_headers(options))
|
231
234
|
|
232
235
|
uri.query = URI.encode_www_form(restype: "container")
|
233
236
|
response = Http.new(uri, headers, signer:).put
|
@@ -239,14 +242,19 @@ module AzureBlob
|
|
239
242
|
def delete_container(options = {})
|
240
243
|
uri = generate_uri(container)
|
241
244
|
uri.query = URI.encode_www_form(restype: "container")
|
242
|
-
response = Http.new(uri, signer:).delete
|
245
|
+
response = Http.new(uri, additional_headers(options), signer:).delete
|
243
246
|
end
|
244
247
|
|
245
248
|
# Return a URI object to a resource in the container. Takes a path.
|
246
249
|
#
|
247
250
|
# Example: +generate_uri("#{container}/#{key}")+
|
248
251
|
def generate_uri(path)
|
249
|
-
|
252
|
+
# https://github.com/Azure/azure-storage-ruby/blob/master/common/lib/azure/storage/common/service/storage_service.rb#L191-L201
|
253
|
+
encoded_path = CGI.escape(path.encode("UTF-8"))
|
254
|
+
encoded_path = encoded_path.gsub(/%2F/, "/")
|
255
|
+
encoded_path = encoded_path.gsub(/%5C/, "/")
|
256
|
+
encoded_path = encoded_path.gsub(/\+/, "%20")
|
257
|
+
URI.parse(File.join(host, encoded_path))
|
250
258
|
end
|
251
259
|
|
252
260
|
# Returns an SAS signed URI
|
@@ -282,7 +290,7 @@ module AzureBlob
|
|
282
290
|
"Content-Type": options[:content_type],
|
283
291
|
"Content-MD5": options[:content_md5],
|
284
292
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
285
|
-
}
|
293
|
+
}.merge(additional_headers(options))
|
286
294
|
|
287
295
|
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(nil)
|
288
296
|
end
|
@@ -301,10 +309,10 @@ module AzureBlob
|
|
301
309
|
uri.query = URI.encode_www_form(comp: "appendblock")
|
302
310
|
|
303
311
|
headers = {
|
304
|
-
"Content-Length": content
|
312
|
+
"Content-Length": content_size(content),
|
305
313
|
"Content-Type": options[:content_type],
|
306
314
|
"Content-MD5": options[:content_md5],
|
307
|
-
}
|
315
|
+
}.merge(additional_headers(options))
|
308
316
|
|
309
317
|
Http.new(uri, headers, signer:).put(content)
|
310
318
|
end
|
@@ -325,10 +333,10 @@ module AzureBlob
|
|
325
333
|
uri.query = URI.encode_www_form(comp: "block", blockid: block_id)
|
326
334
|
|
327
335
|
headers = {
|
328
|
-
"Content-Length": content
|
336
|
+
"Content-Length": content_size(content),
|
329
337
|
"Content-Type": options[:content_type],
|
330
338
|
"Content-MD5": options[:content_md5],
|
331
|
-
}
|
339
|
+
}.merge(additional_headers(options))
|
332
340
|
|
333
341
|
Http.new(uri, headers, signer:).put(content)
|
334
342
|
|
@@ -353,16 +361,21 @@ module AzureBlob
|
|
353
361
|
uri.query = URI.encode_www_form(comp: "blocklist")
|
354
362
|
|
355
363
|
headers = {
|
356
|
-
"Content-Length": content
|
364
|
+
"Content-Length": content_size(content),
|
357
365
|
"Content-Type": options[:content_type],
|
358
366
|
"x-ms-blob-content-md5": options[:content_md5],
|
359
367
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
360
|
-
}
|
368
|
+
}.merge(additional_headers(options))
|
361
369
|
|
362
370
|
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content)
|
363
371
|
end
|
364
372
|
|
365
|
-
|
373
|
+
private
|
374
|
+
|
375
|
+
def additional_headers(options)
|
376
|
+
(options[:headers] || {}).transform_keys { |k| "x-ms-#{k}".to_sym }.
|
377
|
+
transform_values(&:to_s)
|
378
|
+
end
|
366
379
|
|
367
380
|
def generate_block_id(index)
|
368
381
|
Base64.urlsafe_encode64(index.to_s.rjust(6, "0"))
|
@@ -371,7 +384,7 @@ module AzureBlob
|
|
371
384
|
def put_blob_multiple(key, content, options = {})
|
372
385
|
content = StringIO.new(content) if content.is_a? String
|
373
386
|
block_size = options[:block_size] || DEFAULT_BLOCK_SIZE
|
374
|
-
block_count = (content.
|
387
|
+
block_count = (content_size(content).to_f / block_size).ceil
|
375
388
|
block_ids = block_count.times.map do |i|
|
376
389
|
put_blob_block(key, i, content.read(block_size))
|
377
390
|
end
|
@@ -385,15 +398,23 @@ module AzureBlob
|
|
385
398
|
|
386
399
|
headers = {
|
387
400
|
"x-ms-blob-type": "BlockBlob",
|
388
|
-
"Content-Length": content
|
401
|
+
"Content-Length": content_size(content),
|
389
402
|
"Content-Type": options[:content_type],
|
390
403
|
"x-ms-blob-content-md5": options[:content_md5],
|
391
404
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
392
|
-
}
|
405
|
+
}.merge(additional_headers(options))
|
393
406
|
|
394
407
|
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content.read)
|
395
408
|
end
|
396
409
|
|
410
|
+
def content_size(content)
|
411
|
+
if content.respond_to?(:bytesize)
|
412
|
+
content.bytesize
|
413
|
+
else
|
414
|
+
content.size
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
397
418
|
def host
|
398
419
|
@host ||= "https://#{account_name}.blob.#{CLOUD_REGIONS_SUFFIX[cloud_regions]}"
|
399
420
|
end
|
data/lib/azure_blob/http.rb
CHANGED
@@ -19,7 +19,7 @@ module AzureBlob
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
class FileNotFoundError < Error; end
|
22
|
-
class
|
22
|
+
class ForbiddenError < Error; end
|
23
23
|
class IntegrityError < Error; end
|
24
24
|
|
25
25
|
include REXML
|
@@ -94,7 +94,7 @@ module AzureBlob
|
|
94
94
|
|
95
95
|
ERROR_MAPPINGS = {
|
96
96
|
Net::HTTPNotFound => FileNotFoundError,
|
97
|
-
Net::HTTPForbidden =>
|
97
|
+
Net::HTTPForbidden => ForbiddenError,
|
98
98
|
}
|
99
99
|
|
100
100
|
ERROR_CODE_MAPPINGS = {
|
data/lib/azure_blob/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: azure-blob
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joé Dupuis
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
77
|
- !ruby/object:Gem::Version
|
78
78
|
version: '0'
|
79
79
|
requirements: []
|
80
|
-
rubygems_version: 3.
|
80
|
+
rubygems_version: 3.4.19
|
81
81
|
signing_key:
|
82
82
|
specification_version: 4
|
83
83
|
summary: Azure Blob client and Active Storage adapter
|