shrine-storage-azure-blob 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c2474798e685e8b9e30617e60baf5080e0e6cc067a5bd6e90405ac8974764dc6
4
+ data.tar.gz: fdf8a79b46ad3bac570b7f8edfe11018959d7643fd5f64c19f0cc7cee3fd5dcd
5
+ SHA512:
6
+ metadata.gz: 781b3b173790f2a53f971c0ebf866a53991f56272457cd4d67b386ae30c1210917e21d760ea5c3335d2cd8133a10e9994f8c128f199e5a331b477a5005c3109b
7
+ data.tar.gz: 2ac7731ca4a4c52595f7cece4cd1836c84cfacc4befc848f837f3322ae4214d8253b88ff6f7f9f2717411814b84b7dab48f9e1bc6af37d33496c46dbf1ecef56
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-06-22
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Galiia Fattakhova
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Galiia Fattakhova
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # shrine-storage-azure-blob
2
+
3
+ `shrine-storage-azure-blob` is a Shrine storage adapter for Azure Blob Storage.
4
+
5
+ It supports:
6
+
7
+ - direct uploads via short-lived SAS URLs
8
+ - signed private read URLs
9
+ - configurable container prefixes such as `cache/` and `store/`
10
+ - private or public URL generation
11
+
12
+ This adapter is built on top of the [`azure-blob`](https://rubygems.org/gems/azure-blob) gem.
13
+
14
+ ## Installation
15
+
16
+ Install the gem and add to the application's Gemfile by executing:
17
+
18
+ ```bash
19
+ bundle add shrine-storage-azure-blob
20
+ ```
21
+
22
+ If bundler is not being used to manage dependencies, install the gem by executing:
23
+
24
+ ```bash
25
+ gem install shrine-storage-azure-blob
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Require the adapter:
31
+
32
+ ```ruby
33
+ require "shrine/storage/azure_blob"
34
+ ```
35
+
36
+ Configure Shrine storages:
37
+
38
+ ```ruby
39
+ Shrine.storages = {
40
+ cache: Shrine::Storage::AzureBlob.new(
41
+ account_name: ENV.fetch("STORAGE_ACCOUNT_NAME"),
42
+ access_key: ENV.fetch("STORAGE_ACCESS_KEY"),
43
+ container: ENV.fetch("STORAGE_CONTAINER", "uploads"),
44
+ prefix: "cache"
45
+ ),
46
+ store: Shrine::Storage::AzureBlob.new(
47
+ account_name: ENV.fetch("STORAGE_ACCOUNT_NAME"),
48
+ access_key: ENV.fetch("STORAGE_ACCESS_KEY"),
49
+ container: ENV.fetch("STORAGE_CONTAINER", "uploads"),
50
+ prefix: "store"
51
+ )
52
+ }
53
+ ```
54
+
55
+ ### Direct uploads
56
+
57
+ Use `#presign` to generate a short-lived SAS URL for browser uploads:
58
+
59
+ ```ruby
60
+ presign = Shrine.storages[:cache].presign(
61
+ "example.png",
62
+ content_type: "image/png",
63
+ metadata: { filename: "example.png" }
64
+ )
65
+ ```
66
+
67
+ The return value is a hash with:
68
+
69
+ - `:method`
70
+ - `:url`
71
+ - `:headers`
72
+
73
+ The browser should upload the file directly to the returned Azure Blob URL with the returned headers.
74
+
75
+ ### Private file URLs
76
+
77
+ By default, `#url` returns a signed private read URL:
78
+
79
+ ```ruby
80
+ Shrine.storages[:store].url("avatars/user.png")
81
+ ```
82
+
83
+ To generate public URLs instead, initialize the storage with `public: true`.
84
+
85
+ ### Container creation
86
+
87
+ The adapter exposes `#ensure_container!`:
88
+
89
+ ```ruby
90
+ Shrine.storages[:cache].ensure_container!
91
+ ```
92
+
93
+ That is useful for bootstrapping empty environments, but in most deployments container creation should be handled by infrastructure or a deployment task.
94
+
95
+ ### Initialization options
96
+
97
+ Supported initializer options:
98
+
99
+ - `account_name:` Azure Storage Account name
100
+ - `access_key:` Azure Storage Account access key
101
+ - `container:` blob container name
102
+ - `prefix:` optional blob key prefix
103
+ - `host:` optional blob host override
104
+ - `public:` if `true`, `#url` returns unsigned public URLs
105
+
106
+ Additional keyword arguments are forwarded to `AzureBlob::Client`.
107
+
108
+ ## Notes
109
+
110
+ - This adapter currently uses account key authentication.
111
+ - Upload presigning uses Azure SAS URLs with create/write permissions.
112
+ - Private read URLs use signed SAS URLs with read permission.
113
+ - `ActionDispatch::Http::ContentDisposition` is used to format download disposition headers, so `actionpack` is a runtime dependency.
114
+
115
+ ## Development
116
+
117
+ After checking out the repo, run:
118
+
119
+ ```bash
120
+ bin/setup
121
+ bundle exec rake test
122
+ ```
123
+
124
+ You can also run:
125
+
126
+ ```bash
127
+ bin/console
128
+ ```
129
+
130
+ to experiment with the adapter interactively.
131
+
132
+ To install this gem locally:
133
+
134
+ ```bash
135
+ bundle exec rake install
136
+ ```
137
+
138
+ To release a new version:
139
+
140
+ 1. Update the version in `lib/shrine/storage/azure_blob/version.rb`
141
+ 2. Commit the change
142
+ 3. Run:
143
+
144
+ ```bash
145
+ bundle exec rake release
146
+ ```
147
+
148
+ ## Contributing
149
+
150
+ Bug reports and pull requests are welcome on GitHub:
151
+
152
+ - https://github.com/galiyafatt2/shrine-storage-azure-blob
153
+
154
+ ## License
155
+
156
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Storage
5
+ class AzureBlob
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "azure_blob"
4
+ require "action_dispatch/http/content_disposition"
5
+ require "shrine"
6
+ require "stringio"
7
+ require_relative "azure_blob/version"
8
+
9
+ class Shrine
10
+ module Storage
11
+ class AzureBlob
12
+ DEFAULT_URL_TTL = 15 * 60
13
+
14
+ attr_reader :client, :container, :prefix
15
+
16
+ def initialize(account_name:, container:, access_key: nil, host: nil, prefix: nil, public: false, **options)
17
+ @container = container
18
+ @prefix = prefix
19
+ @public = public
20
+ @client = ::AzureBlob::Client.new(
21
+ account_name: account_name,
22
+ access_key: access_key,
23
+ container: container,
24
+ host: host,
25
+ **options
26
+ )
27
+ end
28
+
29
+ def upload(io, id, shrine_metadata: {}, **options)
30
+ client.create_block_blob(
31
+ path_for(id),
32
+ io.respond_to?(:rewind) ? rewindable(io) : io,
33
+ **upload_options(shrine_metadata, options)
34
+ )
35
+ end
36
+
37
+ def open(id, **options)
38
+ StringIO.new(fetch_blob(path_for(id), options)).tap(&:binmode)
39
+ rescue ::AzureBlob::Http::FileNotFoundError => e
40
+ raise Shrine::FileNotFound, e.message
41
+ end
42
+
43
+ def url(id, expires_in: DEFAULT_URL_TTL, filename: nil, disposition: nil, content_type: nil, public: @public, **)
44
+ return client.generate_uri("#{container}/#{path_for(id)}").to_s if public
45
+
46
+ client.signed_uri(
47
+ path_for(id),
48
+ permissions: "r",
49
+ expiry: format_expiry(expires_in),
50
+ content_disposition: content_disposition(disposition, filename),
51
+ content_type: content_type
52
+ ).to_s
53
+ end
54
+
55
+ def exists?(id)
56
+ client.blob_exist?(path_for(id))
57
+ end
58
+
59
+ def delete(id)
60
+ client.delete_blob(path_for(id))
61
+ rescue ::AzureBlob::Http::FileNotFoundError
62
+ nil
63
+ end
64
+
65
+ def delete_prefixed(prefix)
66
+ client.delete_prefix(path_for(prefix))
67
+ end
68
+
69
+ def clear!
70
+ delete_prefixed("")
71
+ end
72
+
73
+ def presign(id, method: "PUT", expires_in: DEFAULT_URL_TTL, content_type: nil, filename: nil, disposition: nil,
74
+ metadata: {}, **)
75
+ headers = {
76
+ "x-ms-blob-type" => "BlockBlob"
77
+ }
78
+ headers["Content-Type"] = content_type if present?(content_type)
79
+
80
+ custom_metadata_headers(metadata).each do |header, value|
81
+ headers[header.to_s] = value
82
+ end
83
+
84
+ if (blob_content_disposition = content_disposition(disposition, filename))
85
+ headers["x-ms-blob-content-disposition"] = blob_content_disposition
86
+ end
87
+
88
+ {
89
+ method: method,
90
+ url: client.signed_uri(path_for(id), permissions: "cw", expiry: format_expiry(expires_in)).to_s,
91
+ headers: headers
92
+ }
93
+ end
94
+
95
+ def ensure_container!
96
+ return if client.container_exist?
97
+
98
+ client.create_container
99
+ end
100
+
101
+ private
102
+
103
+ def path_for(id)
104
+ [prefix, id].compact.reject { |part| blank?(part) }.join("/")
105
+ end
106
+
107
+ def fetch_blob(path, options)
108
+ blob = client.get_blob(path, extract_range_options(options)).dup
109
+ blob.force_encoding(Encoding::BINARY)
110
+ end
111
+
112
+ def extract_range_options(options)
113
+ {}.tap do |range_options|
114
+ range_options[:start] = options[:start] if options.key?(:start)
115
+ range_options[:end] = options[:end] if options.key?(:end)
116
+ range_options[:timeout] = options[:timeout] if options.key?(:timeout)
117
+ end
118
+ end
119
+
120
+ def rewindable(io)
121
+ io.rewind
122
+ io
123
+ end
124
+
125
+ def upload_options(shrine_metadata, options)
126
+ metadata = stringify_metadata(options.fetch(:metadata, {}))
127
+ metadata["filename"] ||= shrine_metadata["filename"] || shrine_metadata[:filename]
128
+
129
+ {
130
+ content_type: shrine_metadata["mime_type"] || shrine_metadata[:mime_type],
131
+ metadata: metadata
132
+ }.merge(compact_hash(reject_keys(options, :metadata)))
133
+ end
134
+
135
+ def stringify_metadata(metadata)
136
+ compact_hash(metadata.to_h.transform_keys(&:to_s).transform_values(&:to_s))
137
+ end
138
+
139
+ def format_expiry(expires_in)
140
+ (Time.now.utc + (expires_in || DEFAULT_URL_TTL)).iso8601
141
+ end
142
+
143
+ def content_disposition(disposition, filename)
144
+ return if blank?(disposition) || blank?(filename)
145
+
146
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
147
+ end
148
+
149
+ def custom_metadata_headers(metadata)
150
+ ::AzureBlob::Metadata.new(stringify_metadata(metadata)).headers
151
+ end
152
+
153
+ def compact_hash(hash)
154
+ hash.reject { |_, value| value.nil? }
155
+ end
156
+
157
+ def reject_keys(hash, *keys)
158
+ hash.reject { |key, _| keys.include?(key) }
159
+ end
160
+
161
+ def blank?(value)
162
+ !present?(value)
163
+ end
164
+
165
+ def present?(value)
166
+ case value
167
+ when nil then false
168
+ when String then !value.empty?
169
+ else true
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shrine/storage/azure_blob"
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shrine-storage-azure-blob
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Galiia Fattakhova
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: azure-blob
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: 0.8.0
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: 0.8.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: shrine
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '4.0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '4.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: mocha
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '2.0'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '2.0'
80
+ description: Provides a Shrine storage adapter for Azure Blob Storage with signed
81
+ upload and download URLs.
82
+ email:
83
+ - elizabeth.mor324@gmail.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - CHANGELOG.md
89
+ - LICENSE
90
+ - LICENSE.txt
91
+ - README.md
92
+ - Rakefile
93
+ - lib/shrine-storage-azure-blob.rb
94
+ - lib/shrine/storage/azure_blob.rb
95
+ - lib/shrine/storage/azure_blob/version.rb
96
+ homepage: https://github.com/galiyafatt2/shrine-storage-azure-blob
97
+ licenses:
98
+ - MIT
99
+ metadata:
100
+ source_code_uri: https://github.com/galiyafatt2/shrine-storage-azure-blob
101
+ changelog_uri: https://github.com/galiyafatt2/shrine-storage-azure-blob/blob/main/CHANGELOG.md
102
+ rubygems_mfa_required: 'true'
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 3.2.0
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.7.2
118
+ specification_version: 4
119
+ summary: Azure Blob Storage adapter for Shrine
120
+ test_files: []