azure-blob 0.8.0 → 0.9.0
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 +17 -2
- data/README.md +31 -0
- data/lib/azure_blob/blob.rb +9 -0
- data/lib/azure_blob/client.rb +74 -5
- data/lib/azure_blob/entra_id_signer.rb +21 -3
- data/lib/azure_blob/http.rb +8 -4
- data/lib/azure_blob/user_delegation_key.rb +25 -7
- data/lib/azure_blob/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e0ce3229ce4281799f9d78b8f5b95343ac3fced270f902f07215630568bec93
|
|
4
|
+
data.tar.gz: ce76f13f260cdc59684ccfe0f655730c6fa03859bfa98264678d111b49999cfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d663e5fb344c55fc2a8efb9247f6207866c9f5676e9e64e028a1bde2c6762fa5bc5dd5ecf9864b41a44176eb7d7782524424a4dc29df1bf3bf716abb47e56d2b
|
|
7
|
+
data.tar.gz: 58663d45654f161db667d2bc5def5a36c7d5202038cf107322805b7f15b12cc03a7b455b058ee11385233a3c1dff70037ffe43118a848e2427e14e177d84920f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.9.0] 2026-10-11
|
|
4
|
+
|
|
5
|
+
- Drop support for Ruby 3.1 and 3.2. Ruby 3.3 or newer is now required
|
|
6
|
+
- Drop support for Rails 7.x. The Active Storage service now supports Rails 8.0 or newer
|
|
7
|
+
- Fix `copy_blob` failing on append blobs and on blobs larger than 256 MiB
|
|
8
|
+
- Fix `compose` failing when its source is a composed blob (#48)
|
|
9
|
+
- Add `put_blob_block_from_url`
|
|
10
|
+
- Add `AzureBlob::Blob#blob_type` and `AzureBlob::Blob#block_blob?`
|
|
11
|
+
- Fix the content type being dropped when a blob is uploaded in multiple blocks
|
|
12
|
+
- Add optional `delegation_key_expiration` to `AzureBlob::Client.new` and the ActiveStorage config to set the minimum lifetime of the Entra ID user delegation key in seconds (default 7 hours, maximum 7 days)
|
|
13
|
+
- Fix Entra ID signed URLs that stopped working before their expiry because the user delegation key expired first
|
|
14
|
+
- Raise `ArgumentError` when signing an Entra ID URL that expires more than 7 days from now
|
|
15
|
+
- Include the HTTP status and Azure error code in `AzureBlob::Http::Error` messages (e.g. `500 InternalError`) and add `AzureBlob::Http::Error#code`
|
|
16
|
+
- Fix error responses without an XML body raising `NoMethodError` instead of `AzureBlob::Http::Error`, which skipped the managed identity token retries
|
|
17
|
+
|
|
18
|
+
## [0.8.0] 2026-01-18
|
|
4
19
|
|
|
5
20
|
- Add cgi gem dependency for Ruby 4.0 compatibility
|
|
6
21
|
|
|
7
|
-
## [0.7.0]
|
|
22
|
+
## [0.7.0] 2026-01-05
|
|
8
23
|
|
|
9
24
|
- Add optional `timeout` parameter to all API methods
|
|
10
25
|
- Add `AzureBlob::Http::TimeoutError` for handling Azure `OperationTimedOut` errors
|
data/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Azure Blob client and Active Storage adapter to replace the now abandoned azure-
|
|
|
4
4
|
|
|
5
5
|
An Active Storage is supplied, but the gem is Rails agnostic and can be used in any Ruby project.
|
|
6
6
|
|
|
7
|
+
Requires Ruby 3.3 or newer. The Active Storage service supports Rails 8.0 or newer.
|
|
8
|
+
|
|
7
9
|
## Active Storage
|
|
8
10
|
|
|
9
11
|
### Migration
|
|
@@ -45,6 +47,35 @@ prod:
|
|
|
45
47
|
principal_id: 71b34410-4c50-451d-b456-95ead1b18cce
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
#### User delegation key expiration
|
|
51
|
+
|
|
52
|
+
When authenticating through Entra ID, signed URLs are signed with a user delegation key
|
|
53
|
+
that AzureBlob requests and renews as needed. `delegation_key_expiration` (in seconds)
|
|
54
|
+
sets how long each key lasts: 7 hours by default, up to Azure's maximum of 7 days. Set it
|
|
55
|
+
above the longest URL lifetime your app issues.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
client = AzureBlob::Client.new(
|
|
59
|
+
account_name: "account_name",
|
|
60
|
+
container: "container_name",
|
|
61
|
+
principal_id: "71b34410-4c50-451d-b456-95ead1b18cce",
|
|
62
|
+
delegation_key_expiration: 86400, # 1 day
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
ActiveStorage config example:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
prod:
|
|
70
|
+
service: AzureBlob
|
|
71
|
+
container: container_name
|
|
72
|
+
storage_account_name: account_name
|
|
73
|
+
principal_id: 71b34410-4c50-451d-b456-95ead1b18cce
|
|
74
|
+
delegation_key_expiration: 86400 # 1 day
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The option is ignored when authenticating with a shared key.
|
|
78
|
+
|
|
48
79
|
#### AKS with Workload Identity
|
|
49
80
|
|
|
50
81
|
ActiveStorage config example:
|
data/lib/azure_blob/blob.rb
CHANGED
|
@@ -28,6 +28,15 @@ module AzureBlob
|
|
|
28
28
|
response.content_length
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Returns the blob type: +"BlockBlob"+, +"AppendBlob"+ or +"PageBlob"+.
|
|
32
|
+
def blob_type
|
|
33
|
+
response["x-ms-blob-type"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def block_blob?
|
|
37
|
+
blob_type == "BlockBlob"
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
def present?
|
|
32
41
|
response.code == "200"
|
|
33
42
|
end
|
data/lib/azure_blob/client.rb
CHANGED
|
@@ -24,6 +24,7 @@ module AzureBlob
|
|
|
24
24
|
@access_key = access_key
|
|
25
25
|
@principal_id = principal_id
|
|
26
26
|
@use_managed_identities = options[:use_managed_identities]
|
|
27
|
+
@delegation_key_expiration = options[:delegation_key_expiration]
|
|
27
28
|
signer unless options[:lazy]
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -81,7 +82,13 @@ module AzureBlob
|
|
|
81
82
|
|
|
82
83
|
# Copy a blob between containers or within the same container
|
|
83
84
|
#
|
|
84
|
-
# Calls to {Copy Blob From URL}[https://learn.microsoft.com/en-us/rest/api/storageservices/copy-blob-from-url]
|
|
85
|
+
# Calls to {Copy Blob From URL}[https://learn.microsoft.com/en-us/rest/api/storageservices/copy-blob-from-url],
|
|
86
|
+
# which copies a block blob of at most 256 MiB in a single request. When Azure refuses the source
|
|
87
|
+
# (an append blob, or a bigger block blob) the blob is copied block by block with
|
|
88
|
+
# {Put Block From URL}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-from-url] instead.
|
|
89
|
+
#
|
|
90
|
+
# Either way the copy is a block blob that keeps the content type, content disposition and checksum
|
|
91
|
+
# of the source, and its metadata unless +:metadata+ is given.
|
|
85
92
|
#
|
|
86
93
|
# Parameters:
|
|
87
94
|
# - key: destination blob path
|
|
@@ -89,9 +96,11 @@ module AzureBlob
|
|
|
89
96
|
# - options: additional options
|
|
90
97
|
# - source_client: AzureBlob::Client instance for the source container (optional)
|
|
91
98
|
# If not provided, copies from within the same container
|
|
99
|
+
# - metadata: metadata of the copy (optional). Defaults to the metadata of the source
|
|
100
|
+
# - block_size: block size of a block by block copy. Defaults to +AzureBlob::DEFAULT_BLOCK_SIZE+
|
|
92
101
|
#
|
|
93
102
|
def copy_blob(key, source_key, options = {})
|
|
94
|
-
source_client = options
|
|
103
|
+
source_client = options[:source_client] || self
|
|
95
104
|
uri = generate_uri("#{container}/#{key}")
|
|
96
105
|
uri.query = URI.encode_www_form(timeout: options[:timeout]) if options[:timeout]
|
|
97
106
|
|
|
@@ -103,6 +112,9 @@ module AzureBlob
|
|
|
103
112
|
}.merge(additional_headers(options))
|
|
104
113
|
|
|
105
114
|
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put
|
|
115
|
+
rescue Http::Error => error
|
|
116
|
+
raise unless error.status == 409
|
|
117
|
+
copy_blob_blocks(key, source_key, source_client, options)
|
|
106
118
|
end
|
|
107
119
|
|
|
108
120
|
# Delete a blob
|
|
@@ -361,6 +373,35 @@ module AzureBlob
|
|
|
361
373
|
block_id
|
|
362
374
|
end
|
|
363
375
|
|
|
376
|
+
# Copies a block from a URL into a blob.
|
|
377
|
+
#
|
|
378
|
+
# Calls to {Put Block From URL}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-from-url]
|
|
379
|
+
#
|
|
380
|
+
# Returns the id of the block. Required to commit the list of blocks to a blob.
|
|
381
|
+
#
|
|
382
|
+
# Options:
|
|
383
|
+
#
|
|
384
|
+
# [+:source_range+]
|
|
385
|
+
# Range of bytes to copy from the source, for example +0..(block_size - 1)+. Defaults to the whole source.
|
|
386
|
+
def put_blob_block_from_url(key, index, source_uri, options = {})
|
|
387
|
+
block_id = generate_block_id(index)
|
|
388
|
+
uri = generate_uri("#{container}/#{key}")
|
|
389
|
+
query = { comp: "block", blockid: block_id }
|
|
390
|
+
query[:timeout] = options[:timeout] if options[:timeout]
|
|
391
|
+
uri.query = URI.encode_www_form(**query)
|
|
392
|
+
|
|
393
|
+
source_range = options[:source_range]
|
|
394
|
+
headers = {
|
|
395
|
+
"Content-Length": 0,
|
|
396
|
+
"x-ms-copy-source": source_uri.to_s,
|
|
397
|
+
"x-ms-source-range": source_range && "bytes=#{source_range.min}-#{source_range.max}",
|
|
398
|
+
}.merge(additional_headers(options))
|
|
399
|
+
|
|
400
|
+
Http.new(uri, headers, signer:).put
|
|
401
|
+
|
|
402
|
+
block_id
|
|
403
|
+
end
|
|
404
|
+
|
|
364
405
|
# Commits the list of blocks to a blob.
|
|
365
406
|
#
|
|
366
407
|
# Calls to {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list]
|
|
@@ -382,7 +423,7 @@ module AzureBlob
|
|
|
382
423
|
|
|
383
424
|
headers = {
|
|
384
425
|
"Content-Length": content_size(content),
|
|
385
|
-
"
|
|
426
|
+
"x-ms-blob-content-type": options[:content_type],
|
|
386
427
|
"x-ms-blob-content-md5": options[:content_md5],
|
|
387
428
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
|
388
429
|
}.merge(additional_headers(options))
|
|
@@ -428,6 +469,34 @@ module AzureBlob
|
|
|
428
469
|
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content.read)
|
|
429
470
|
end
|
|
430
471
|
|
|
472
|
+
# One Put Block From URL request per block, then one Put Block List to commit them.
|
|
473
|
+
# Works with any source blob type and size, and carries the properties and metadata over itself.
|
|
474
|
+
def copy_blob_blocks(key, source_key, source_client, options)
|
|
475
|
+
source = source_client.get_blob_properties(source_key, options.slice(:timeout))
|
|
476
|
+
block_size = options[:block_size] || DEFAULT_BLOCK_SIZE
|
|
477
|
+
# Every block reads the source through this URI, so it has to outlive the whole copy.
|
|
478
|
+
source_uri = source_client.signed_uri(source_key, permissions: "r", expiry: Time.at(Time.now.to_i + 3600).utc.iso8601)
|
|
479
|
+
|
|
480
|
+
block_ids = (source.size.to_f / block_size).ceil.times.map do |index|
|
|
481
|
+
first = index * block_size
|
|
482
|
+
last = [ first + block_size, source.size ].min - 1
|
|
483
|
+
put_blob_block_from_url(key, index, source_uri, source_range: first..last, **options.slice(:timeout, :headers))
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
metadata = options[:metadata]
|
|
487
|
+
metadata = source.metadata if metadata.nil? || metadata.empty?
|
|
488
|
+
|
|
489
|
+
commit_blob_blocks(
|
|
490
|
+
key,
|
|
491
|
+
block_ids,
|
|
492
|
+
content_type: source.content_type,
|
|
493
|
+
content_md5: source.checksum,
|
|
494
|
+
content_disposition: source.content_disposition,
|
|
495
|
+
metadata: metadata,
|
|
496
|
+
**options.slice(:tags, :timeout, :headers),
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
431
500
|
def content_size(content)
|
|
432
501
|
if content.respond_to?(:bytesize)
|
|
433
502
|
content.bytesize
|
|
@@ -453,11 +522,11 @@ module AzureBlob
|
|
|
453
522
|
end
|
|
454
523
|
|
|
455
524
|
using_managed_identities ?
|
|
456
|
-
AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id:) :
|
|
525
|
+
AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id:, delegation_key_expiration:) :
|
|
457
526
|
AzureBlob::SharedKeySigner.new(account_name:, access_key:, host:)
|
|
458
527
|
end
|
|
459
528
|
end
|
|
460
529
|
|
|
461
|
-
attr_reader :account_name, :container, :http, :cloud_regions, :access_key, :principal_id, :use_managed_identities
|
|
530
|
+
attr_reader :account_name, :container, :http, :cloud_regions, :access_key, :principal_id, :use_managed_identities, :delegation_key_expiration
|
|
462
531
|
end
|
|
463
532
|
end
|
|
@@ -14,7 +14,8 @@ module AzureBlob
|
|
|
14
14
|
attr_reader :account_name
|
|
15
15
|
attr_reader :host
|
|
16
16
|
|
|
17
|
-
def initialize(account_name:, host:, principal_id: nil)
|
|
17
|
+
def initialize(account_name:, host:, principal_id: nil, delegation_key_expiration: nil)
|
|
18
|
+
@delegation_key_expiration = parse_delegation_key_expiration(delegation_key_expiration)
|
|
18
19
|
@token = AzureBlob::IdentityToken.new(principal_id:)
|
|
19
20
|
@account_name = account_name
|
|
20
21
|
@host = host
|
|
@@ -25,7 +26,7 @@ module AzureBlob
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def sas_token(uri, options = {})
|
|
28
|
-
delegation_key.refresh
|
|
29
|
+
delegation_key.refresh(valid_until: options[:expiry])
|
|
29
30
|
to_sign = [
|
|
30
31
|
options[:permissions],
|
|
31
32
|
options[:start],
|
|
@@ -82,8 +83,25 @@ module AzureBlob
|
|
|
82
83
|
|
|
83
84
|
private
|
|
84
85
|
|
|
86
|
+
attr_reader :delegation_key_expiration
|
|
87
|
+
|
|
88
|
+
def parse_delegation_key_expiration(value)
|
|
89
|
+
return if value.nil?
|
|
90
|
+
|
|
91
|
+
seconds = Integer(value, exception: false)
|
|
92
|
+
unless seconds&.positive?
|
|
93
|
+
raise ArgumentError, "delegation_key_expiration must be a positive number of seconds, got #{value.inspect}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if seconds > UserDelegationKey::MAX_EXPIRATION
|
|
97
|
+
raise ArgumentError, "delegation_key_expiration cannot be greater than #{UserDelegationKey::MAX_EXPIRATION} seconds (7 days), got #{value.inspect}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
seconds
|
|
101
|
+
end
|
|
102
|
+
|
|
85
103
|
def delegation_key
|
|
86
|
-
@delegation_key ||= UserDelegationKey.new(account_name:, signer: self)
|
|
104
|
+
@delegation_key ||= UserDelegationKey.new(account_name:, signer: self, expiration: delegation_key_expiration || UserDelegationKey::EXPIRATION)
|
|
87
105
|
end
|
|
88
106
|
|
|
89
107
|
def sign(body, key:)
|
data/lib/azure_blob/http.rb
CHANGED
|
@@ -8,10 +8,12 @@ require "rexml"
|
|
|
8
8
|
module AzureBlob
|
|
9
9
|
class Http # :nodoc:
|
|
10
10
|
class Error < AzureBlob::Error
|
|
11
|
-
attr_reader :body, :status
|
|
12
|
-
def initialize(body: nil, status: nil)
|
|
11
|
+
attr_reader :body, :status, :code
|
|
12
|
+
def initialize(body: nil, status: nil, code: nil)
|
|
13
13
|
@body = body
|
|
14
14
|
@status = status
|
|
15
|
+
@code = code
|
|
16
|
+
super(status && [ status, code ].compact.join(" "))
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def inspect
|
|
@@ -118,7 +120,7 @@ module AzureBlob
|
|
|
118
120
|
|
|
119
121
|
def raise_error
|
|
120
122
|
return unless raise_on_error
|
|
121
|
-
raise error_from_response.new(body: @response.body, status: @response.code&.to_i)
|
|
123
|
+
raise error_from_response.new(body: @response.body, status: @response.code&.to_i, code: azure_error_code)
|
|
122
124
|
end
|
|
123
125
|
|
|
124
126
|
def status
|
|
@@ -126,7 +128,9 @@ module AzureBlob
|
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def azure_error_code
|
|
129
|
-
Document.new(response.body).get_elements("//Error/Code").first
|
|
131
|
+
response["x-ms-error-code"] || Document.new(response.body.to_s).get_elements("//Error/Code").first&.text
|
|
132
|
+
rescue ParseException
|
|
133
|
+
nil
|
|
130
134
|
end
|
|
131
135
|
|
|
132
136
|
def error_from_response
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
1
3
|
require_relative "http"
|
|
2
4
|
|
|
3
5
|
module AzureBlob
|
|
4
6
|
class UserDelegationKey # :nodoc:
|
|
5
7
|
EXPIRATION = 25200 # 7 hours
|
|
8
|
+
MAX_EXPIRATION = 604800 # 7 days
|
|
6
9
|
EXPIRATION_BUFFER = 3600 # 1 hours
|
|
7
|
-
|
|
10
|
+
|
|
11
|
+
def initialize(account_name:, signer:, expiration: EXPIRATION)
|
|
8
12
|
@uri = URI.parse(
|
|
9
13
|
"#{signer.host}/?restype=service&comp=userdelegationkey"
|
|
10
14
|
)
|
|
11
15
|
|
|
12
16
|
@signer = signer
|
|
17
|
+
@expiration_duration = expiration
|
|
13
18
|
|
|
14
19
|
refresh
|
|
15
20
|
end
|
|
@@ -19,14 +24,22 @@ module AzureBlob
|
|
|
19
24
|
user_delegation_key
|
|
20
25
|
end
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
# Requests a new key when this one is about to expire, or when it expires
|
|
28
|
+
# before +valid_until+ (the expiry of the SAS about to be signed with it).
|
|
29
|
+
def refresh(valid_until: nil)
|
|
30
|
+
valid_until = Time.parse(valid_until) if valid_until.is_a?(String)
|
|
24
31
|
now = Time.now.utc
|
|
25
32
|
|
|
33
|
+
if valid_until && valid_until > now + MAX_EXPIRATION
|
|
34
|
+
raise ArgumentError, "signed URL expiry #{valid_until.getutc.iso8601} is more than #{MAX_EXPIRATION} seconds (7 days) away, " \
|
|
35
|
+
"Azure cannot sign a user delegation SAS that lasts longer than 7 days"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return unless expired? || (valid_until && valid_until > expiration)
|
|
26
39
|
|
|
27
40
|
start = now.iso8601
|
|
28
|
-
|
|
29
|
-
expiry =
|
|
41
|
+
new_expiration = [ now + expiration_duration, valid_until ].compact.max
|
|
42
|
+
expiry = new_expiration.getutc.iso8601
|
|
30
43
|
|
|
31
44
|
content = <<-XML.gsub!(/[[:space:]]+/, " ").strip!
|
|
32
45
|
<?xml version="1.0" encoding="utf-8"?>
|
|
@@ -47,6 +60,7 @@ module AzureBlob
|
|
|
47
60
|
@signed_service = doc.get_elements("/UserDelegationKey/SignedService").first.get_text.to_s
|
|
48
61
|
@signed_version = doc.get_elements("/UserDelegationKey/SignedVersion").first.get_text.to_s
|
|
49
62
|
@user_delegation_key = Base64.decode64(doc.get_elements("/UserDelegationKey/Value").first.get_text.to_s)
|
|
63
|
+
@expiration = new_expiration
|
|
50
64
|
end
|
|
51
65
|
|
|
52
66
|
attr_reader :signed_oid,
|
|
@@ -60,9 +74,13 @@ module AzureBlob
|
|
|
60
74
|
private
|
|
61
75
|
|
|
62
76
|
def expired?
|
|
63
|
-
expiration.nil? || Time.now >= (expiration -
|
|
77
|
+
expiration.nil? || Time.now >= (expiration - refresh_buffer)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def refresh_buffer
|
|
81
|
+
[ EXPIRATION_BUFFER, expiration_duration / 2 ].min
|
|
64
82
|
end
|
|
65
83
|
|
|
66
|
-
attr_reader :uri, :user_delegation_key, :signer, :expiration
|
|
84
|
+
attr_reader :uri, :user_delegation_key, :signer, :expiration, :expiration_duration
|
|
67
85
|
end
|
|
68
86
|
end
|
data/lib/azure_blob/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: azure-blob
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joé Dupuis
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cgi
|
|
@@ -86,14 +86,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - ">="
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '3.
|
|
89
|
+
version: '3.3'
|
|
90
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
91
|
requirements:
|
|
92
92
|
- - ">="
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
94
|
version: '0'
|
|
95
95
|
requirements: []
|
|
96
|
-
rubygems_version: 3.
|
|
96
|
+
rubygems_version: 3.5.22
|
|
97
97
|
signing_key:
|
|
98
98
|
specification_version: 4
|
|
99
99
|
summary: Azure Blob client and Active Storage adapter
|