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 +4 -4
- data/CHANGELOG.md +11 -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 +53 -26
- 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,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
|
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,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
|
-
#
|
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}
|
192
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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.
|
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
|
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
|
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
|