azure-blob 0.5.7.1 → 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: b05479c9f6ba44b76af4d096021a2a887e8774caf4dcd1c2e02a98c8a2998426
4
- data.tar.gz: 75c680b7c740b541e078cb9a1fc4f7c213719abfa68d3057a21982a1a0977320
3
+ metadata.gz: acd470dcd917d78f24d3a0c6b32f894ea174d2ec774f322a151b64175371ecfd
4
+ data.tar.gz: dbde6c98095cf2ca14acc2478660f22cdd503c6a84f931fb0935768d4f4a68fa
5
5
  SHA512:
6
- metadata.gz: 1116f9eec87e05685dd8177f400cc322b1a393e0d96eaf91bfe52cfb465ebc7ded753ff1c040f9dbed031038c6cca2cab80bd7eec591d4e020ac151c7a1b59af
7
- data.tar.gz: 76a9a8840903de54253fce878ccee22ba55be7097f018c171a3b6863862fa19e2656475dbf415b609426a428212acb14d35117c55c6d8905060f3909be28d61a
6
+ metadata.gz: b04b7147d27d49480a3e8c8ffb8e22362bd902be6f57550c2dc79570893d6101016d16c30e4014b6af1d09eeb6b40a7790e17b226776963d8e9b76b06d1d43cf
7
+ data.tar.gz: '0380bcadb8158e353716dcbc0d276a41a485d9866fa6e0deb0fb34df24654686cf1fe9fb51e58d228fbb39c75f061d5be1ef6895df11cd809f727e050bd49473'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
10
+ ## [0.5.8] 2025-05-14
11
+
12
+ - Add support for copying blobs across containers (#24)
13
+
3
14
  ## [0.5.7.1] 2025-04-22
4
15
 
5
16
  - Fixed a bug when reusing the account name in the container name. #22
@@ -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,26 +73,32 @@ 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
79
80
 
80
- # Copy a blob
81
+ # Copy a blob between containers or within the same container
81
82
  #
82
83
  # Calls to {Copy Blob From URL}[https://learn.microsoft.com/en-us/rest/api/storageservices/copy-blob-from-url]
83
84
  #
84
- # Takes a key (path) and a source_key (path).
85
+ # Parameters:
86
+ # - key: destination blob path
87
+ # - source_key: source blob path
88
+ # - options: additional options
89
+ # - source_client: AzureBlob::Client instance for the source container (optional)
90
+ # If not provided, copies from within the same container
85
91
  #
86
92
  def copy_blob(key, source_key, options = {})
93
+ source_client = options.delete(:source_client) || self
87
94
  uri = generate_uri("#{container}/#{key}")
88
95
 
89
- source_uri = signed_uri(source_key, permissions: "r", expiry: Time.at(Time.now.to_i + 300).utc.iso8601)
96
+ source_uri = source_client.signed_uri(source_key, permissions: "r", expiry: Time.at(Time.now.to_i + 300).utc.iso8601)
90
97
 
91
98
  headers = {
92
99
  "x-ms-copy-source": source_uri.to_s,
93
100
  "x-ms-requires-sync": "true",
94
- }
101
+ }.merge(additional_headers(options))
95
102
 
96
103
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put
97
104
  end
@@ -110,7 +117,7 @@ module AzureBlob
110
117
 
111
118
  headers = {
112
119
  "x-ms-delete-snapshots": options[:delete_snapshots] || "include",
113
- }
120
+ }.merge(additional_headers(options))
114
121
 
115
122
  Http.new(uri, headers, signer:).delete
116
123
  end
@@ -151,7 +158,7 @@ module AzureBlob
151
158
  query[:marker] = marker
152
159
  query.reject! { |key, value| value.to_s.empty? }
153
160
  uri.query = URI.encode_www_form(**query)
154
- response = Http.new(uri, signer:).get
161
+ response = Http.new(uri, additional_headers(options), signer:).get
155
162
  end
156
163
 
157
164
  BlobList.new(fetcher)
@@ -166,7 +173,7 @@ module AzureBlob
166
173
  def get_blob_properties(key, options = {})
167
174
  uri = generate_uri("#{container}/#{key}")
168
175
 
169
- response = Http.new(uri, signer:).head
176
+ response = Http.new(uri, additional_headers(options), signer:).head
170
177
 
171
178
  Blob.new(response)
172
179
  end
@@ -187,9 +194,10 @@ module AzureBlob
187
194
  # Takes a key (path) of the blob.
188
195
  #
189
196
  # Returns a hash of the blob's tags.
190
- def get_blob_tags(key)
191
- uri = generate_uri("#{container}/#{key}?comp=tags")
192
- 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
193
201
 
194
202
  Tags.from_response(response).to_h
195
203
  end
@@ -202,7 +210,7 @@ module AzureBlob
202
210
  def get_container_properties(options = {})
203
211
  uri = generate_uri(container)
204
212
  uri.query = URI.encode_www_form(restype: "container")
205
- response = Http.new(uri, signer:, raise_on_error: false).head
213
+ response = Http.new(uri, additional_headers(options), signer:, raise_on_error: false).head
206
214
 
207
215
  Container.new(response)
208
216
  end
@@ -222,6 +230,7 @@ module AzureBlob
222
230
  headers = {}
223
231
  headers[:"x-ms-blob-public-access"] = "blob" if options[:public_access]
224
232
  headers[:"x-ms-blob-public-access"] = options[:public_access] if [ "container", "blob" ].include?(options[:public_access])
233
+ headers.merge!(additional_headers(options))
225
234
 
226
235
  uri.query = URI.encode_www_form(restype: "container")
227
236
  response = Http.new(uri, headers, signer:).put
@@ -233,14 +242,19 @@ module AzureBlob
233
242
  def delete_container(options = {})
234
243
  uri = generate_uri(container)
235
244
  uri.query = URI.encode_www_form(restype: "container")
236
- response = Http.new(uri, signer:).delete
245
+ response = Http.new(uri, additional_headers(options), signer:).delete
237
246
  end
238
247
 
239
248
  # Return a URI object to a resource in the container. Takes a path.
240
249
  #
241
250
  # Example: +generate_uri("#{container}/#{key}")+
242
251
  def generate_uri(path)
243
- 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))
244
258
  end
245
259
 
246
260
  # Returns an SAS signed URI
@@ -276,7 +290,7 @@ module AzureBlob
276
290
  "Content-Type": options[:content_type],
277
291
  "Content-MD5": options[:content_md5],
278
292
  "x-ms-blob-content-disposition": options[:content_disposition],
279
- }
293
+ }.merge(additional_headers(options))
280
294
 
281
295
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(nil)
282
296
  end
@@ -295,10 +309,10 @@ module AzureBlob
295
309
  uri.query = URI.encode_www_form(comp: "appendblock")
296
310
 
297
311
  headers = {
298
- "Content-Length": content.size,
312
+ "Content-Length": content_size(content),
299
313
  "Content-Type": options[:content_type],
300
314
  "Content-MD5": options[:content_md5],
301
- }
315
+ }.merge(additional_headers(options))
302
316
 
303
317
  Http.new(uri, headers, signer:).put(content)
304
318
  end
@@ -319,10 +333,10 @@ module AzureBlob
319
333
  uri.query = URI.encode_www_form(comp: "block", blockid: block_id)
320
334
 
321
335
  headers = {
322
- "Content-Length": content.size,
336
+ "Content-Length": content_size(content),
323
337
  "Content-Type": options[:content_type],
324
338
  "Content-MD5": options[:content_md5],
325
- }
339
+ }.merge(additional_headers(options))
326
340
 
327
341
  Http.new(uri, headers, signer:).put(content)
328
342
 
@@ -347,16 +361,21 @@ module AzureBlob
347
361
  uri.query = URI.encode_www_form(comp: "blocklist")
348
362
 
349
363
  headers = {
350
- "Content-Length": content.size,
364
+ "Content-Length": content_size(content),
351
365
  "Content-Type": options[:content_type],
352
366
  "x-ms-blob-content-md5": options[:content_md5],
353
367
  "x-ms-blob-content-disposition": options[:content_disposition],
354
- }
368
+ }.merge(additional_headers(options))
355
369
 
356
370
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content)
357
371
  end
358
372
 
359
- 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
360
379
 
361
380
  def generate_block_id(index)
362
381
  Base64.urlsafe_encode64(index.to_s.rjust(6, "0"))
@@ -365,7 +384,7 @@ module AzureBlob
365
384
  def put_blob_multiple(key, content, options = {})
366
385
  content = StringIO.new(content) if content.is_a? String
367
386
  block_size = options[:block_size] || DEFAULT_BLOCK_SIZE
368
- block_count = (content.size.to_f / block_size).ceil
387
+ block_count = (content_size(content).to_f / block_size).ceil
369
388
  block_ids = block_count.times.map do |i|
370
389
  put_blob_block(key, i, content.read(block_size))
371
390
  end
@@ -379,15 +398,23 @@ module AzureBlob
379
398
 
380
399
  headers = {
381
400
  "x-ms-blob-type": "BlockBlob",
382
- "Content-Length": content.size,
401
+ "Content-Length": content_size(content),
383
402
  "Content-Type": options[:content_type],
384
403
  "x-ms-blob-content-md5": options[:content_md5],
385
404
  "x-ms-blob-content-disposition": options[:content_disposition],
386
- }
405
+ }.merge(additional_headers(options))
387
406
 
388
407
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content.read)
389
408
  end
390
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
+
391
418
  def host
392
419
  @host ||= "https://#{account_name}.blob.#{CLOUD_REGIONS_SUFFIX[cloud_regions]}"
393
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.7.1"
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.7.1
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