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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c28844e3daa4fffbd9cf4158de8ef573ee1fc204ef765cb8f3315bfb2fd6838d
4
- data.tar.gz: f8d2422437463cb5d86b83fa0a52efca9a8babd8ab61666551b4c8b7c7c8df9d
3
+ metadata.gz: acd470dcd917d78f24d3a0c6b32f894ea174d2ec774f322a151b64175371ecfd
4
+ data.tar.gz: dbde6c98095cf2ca14acc2478660f22cdd503c6a84f931fb0935768d4f4a68fa
5
5
  SHA512:
6
- metadata.gz: 13d10f1bb9a116684fdfdd5a00ffcc870a6a0976acd4a57478be1b47419a3f406d892f2ce83205a601fcf9b216e742033732695ac6e22336ca9c4dd7dc8d9156
7
- data.tar.gz: 4d998b1b6e9cb61249d9c6aa5618fb13ac4f7bd65ad6c0e34d9c159b08431059de0a5282262edcb08a759b6816ea9049013273a6eee7effa79ff13a8192588d0
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)
@@ -61,7 +61,7 @@ module AzureBlob
61
61
  def current_page
62
62
  document
63
63
  .get_elements("//EnumerationResults/Blobs/Blob/Name")
64
- .map { |element| element.get_text.to_s }
64
+ .map { |element| element.text }
65
65
  end
66
66
 
67
67
  def fetch
@@ -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::DEFAULT_PARSER.unescape(uri.path)
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)
@@ -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.size > (options[:block_size] || DEFAULT_BLOCK_SIZE)
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}?comp=tags")
198
- response = Http.new(uri, signer:).get
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
- URI.parse(URI::DEFAULT_PARSER.escape(File.join(host, path)))
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.size,
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.size,
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.size,
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
- private
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.size.to_f / block_size).ceil
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.size,
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
@@ -19,7 +19,7 @@ module AzureBlob
19
19
  end
20
20
  end
21
21
  class FileNotFoundError < Error; end
22
- class ForbidenError < Error; end
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 => ForbidenError,
97
+ Net::HTTPForbidden => ForbiddenError,
98
98
  }
99
99
 
100
100
  ERROR_CODE_MAPPINGS = {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AzureBlob
4
- VERSION = "0.5.8"
4
+ VERSION = "0.5.9"
5
5
  end
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.8
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.3.27
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