jd-paperclip-azure 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +25 -0
  3. data/.github/workflows/tests.yml +22 -0
  4. data/.gitignore +58 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +3 -0
  7. data/History.txt +6 -0
  8. data/Manifest.txt +9 -0
  9. data/README.md +138 -0
  10. data/Rakefile +29 -0
  11. data/lib/paperclip/azure.rb +5 -0
  12. data/lib/paperclip/storage/azure/environment.rb +19 -0
  13. data/lib/paperclip/storage/azure.rb +271 -0
  14. data/lib/paperclip-azure.rb +1 -0
  15. data/paperclip-azure.gemspec +33 -0
  16. data/spec/database.yml +3 -0
  17. data/spec/paperclip/storage/azure/environment_spec.rb +26 -0
  18. data/spec/paperclip/storage/azure_spec.rb +426 -0
  19. data/spec/spec_helper.rb +40 -0
  20. data/spec/support/fake_model.rb +25 -0
  21. data/spec/support/fake_rails.rb +12 -0
  22. data/spec/support/fixtures/12k.png +0 -0
  23. data/spec/support/fixtures/50x50.png +0 -0
  24. data/spec/support/fixtures/5k.png +0 -0
  25. data/spec/support/fixtures/animated +0 -0
  26. data/spec/support/fixtures/animated.gif +0 -0
  27. data/spec/support/fixtures/animated.unknown +0 -0
  28. data/spec/support/fixtures/azure.yml +8 -0
  29. data/spec/support/fixtures/bad.png +1 -0
  30. data/spec/support/fixtures/empty.html +1 -0
  31. data/spec/support/fixtures/empty.xlsx +0 -0
  32. data/spec/support/fixtures/fog.yml +8 -0
  33. data/spec/support/fixtures/rotated.jpg +0 -0
  34. data/spec/support/fixtures/spaced file.jpg +0 -0
  35. data/spec/support/fixtures/spaced file.png +0 -0
  36. data/spec/support/fixtures/text.txt +1 -0
  37. data/spec/support/fixtures/twopage.pdf +0 -0
  38. data/spec/support/fixtures/uppercase.PNG +0 -0
  39. data/spec/support/mock_attachment.rb +24 -0
  40. data/spec/support/mock_interpolator.rb +24 -0
  41. data/spec/support/mock_url_generator_builder.rb +27 -0
  42. data/spec/support/model_reconstruction.rb +68 -0
  43. data/spec/support/test_data.rb +13 -0
  44. data/spec/support/version_helper.rb +9 -0
  45. metadata +256 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bfc4bcb85a94ff22f13b24dcfc41e6de1b6db1a99507944da8f8482b98ffcfef
4
+ data.tar.gz: 9f3d9ffcf35af0ab3690933d1e2fe500c4008070e2673e8d55a46de21cd9a348
5
+ SHA512:
6
+ metadata.gz: 9358e35a59dd40553ee0f19949a8af4b3e7125f9791f8e049924af7cd183add4bdc60c9791d74eba9b7c3ef5a20f3537aa6a43811348b50d5c5f9a0744f2bd45
7
+ data.tar.gz: a7f025569b498c62dc86fa337270f0853214e71658753f2198261c24f0ebfefbb277b502bb9a169ade65e1978909d36946c9e60181ee7750dbe26f5fed674887
data/.autotest ADDED
@@ -0,0 +1,25 @@
1
+ # -*- ruby -*-
2
+
3
+ require "autotest/restart"
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.testlib = "minitest/unit"
7
+ #
8
+ # at.extra_files << "../some/external/dependency.rb"
9
+ #
10
+ # at.libs << ":../some/external"
11
+ #
12
+ # at.add_exception "vendor"
13
+ #
14
+ # at.add_mapping(/dependency.rb/) do |f, _|
15
+ # at.files_matching(/test_.*rb$/)
16
+ # end
17
+ #
18
+ # %w(TestA TestB).each do |klass|
19
+ # at.extra_class_map[klass] = "test/test_misc.rb"
20
+ # end
21
+ # end
22
+
23
+ # Autotest.add_hook :run_command do |at|
24
+ # system "rake build"
25
+ # end
@@ -0,0 +1,22 @@
1
+ name: rspec test suite
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+ pull_request:
7
+ branches:
8
+ - master
9
+ jobs:
10
+ test:
11
+ name: "💎 rspec"
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: '3.2.1'
19
+ - name: Install dependencies
20
+ run: bundle install
21
+ - name: Run tests
22
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,58 @@
1
+
2
+ # Created by https://www.gitignore.io/api/ruby
3
+
4
+ ### Ruby ###
5
+ *.gem
6
+ *.rbc
7
+ /.config
8
+ /coverage/
9
+ /Gemfile.lock
10
+ /InstalledFiles
11
+ /pkg/
12
+ /spec/debug.log
13
+ /spec/reports/
14
+ /spec/examples.txt
15
+ /test/tmp/
16
+ /test/version_tmp/
17
+ /tmp/
18
+
19
+ # Used by dotenv library to load environment variables.
20
+ # .env
21
+
22
+ ## Specific to RubyMotion:
23
+ .dat*
24
+ .repl_history
25
+ build/
26
+ *.bridgesupport
27
+ build-iPhoneOS/
28
+ build-iPhoneSimulator/
29
+
30
+ ## Specific to RubyMotion (use of CocoaPods):
31
+ #
32
+ # We recommend against adding the Pods directory to your .gitignore. However
33
+ # you should judge for yourself, the pros and cons are mentioned at:
34
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
35
+ #
36
+ # vendor/Pods/
37
+
38
+ ## Documentation cache and generated files:
39
+ /.yardoc/
40
+ /_yardoc/
41
+ /doc/
42
+ /rdoc/
43
+
44
+ ## Environment normalization:
45
+ /.bundle/
46
+ /vendor/bundle
47
+ /lib/bundler/man/
48
+
49
+ # for a library or gem, you might want to ignore these files since the code is
50
+ # intended to run in multiple environments; otherwise, check them in:
51
+ # Gemfile.lock
52
+ # .ruby-version
53
+ # .ruby-gemset
54
+
55
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
56
+ .rvmrc
57
+
58
+ # End of https://www.gitignore.io/api/ruby
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.1
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2017-07-24
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,9 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.md
5
+ Rakefile
6
+ lib/paperclip/storage/azure/environment.rb
7
+ lib/paperclip/storage/azure.rb
8
+ lib/paperclip/azure.rb
9
+ lib/paperclip-azure.rb
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ = paperclip-azure
2
+
3
+ home :: https://github.com/mistydemeo/paperclip-azure
4
+ code :: https://github.com/mistydemeo/paperclip-azure
5
+ bugs :: https://github.com/mistydemeo/paperclip-azure/issues
6
+
7
+ == DESCRIPTION:
8
+
9
+ Paperclip-Azure is a [Paperclip](https://github.com/thoughtbot/paperclip) storage driver for storing files in a Microsoft Azure Blob. This is a friendly fork of the [original gem](https://github.com/supportify/paperclip-azure); it incorporates bugfixes that haven't yet been incldued in point releases of the upstream gem and some new features.
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ * FIX (list of features or problems)
14
+
15
+ == SYNOPSIS:
16
+
17
+ The Azure storage engine has been developed to work as similarly to S3 storage configuration as is possible. This gem can be configured in a Paperclip initializer or environment file as follows:
18
+
19
+ Paperclip::Attachment.default_options[:storage] = :azure
20
+ Paperclip::Attachment.default_options[:azure_credentials] = {
21
+ storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
22
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
23
+ container: ENV['AZURE_CONTAINER_NAME']
24
+ }
25
+
26
+ Or, at the level of the model such as in the following example:
27
+
28
+ has_attached_file :download,
29
+ storage: :azure,
30
+ azure_credentials: {
31
+ storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
32
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
33
+ container: ENV['AZURE_CONTAINER_NAME']
34
+ }
35
+
36
+ Additionally, you can also supply credentials using a path or a File that contains the +storage_access_key+ and +storage_account_name+ that Azure gives you. You can 'environment-space' this just like you do to your `database.yml` file, so different environments can use different accounts:
37
+
38
+ development:
39
+ storage_account_name: foo
40
+ storage_access_key: 123...
41
+ test:
42
+ storage_account_name: foo
43
+ storage_access_key: abc...
44
+ production:
45
+ storage_account_name: foo
46
+ storage_access_key: 456...
47
+
48
+ This is not required, however, and the file may simply look like this:
49
+
50
+ storage_account_name: foo
51
+ storage_access_key: 456...
52
+
53
+ In which case, those access keys will be used in all environments. You can also put your container name in this file, instead of adding it to the code directly. This is useful when you want the same account but a different container for development versus production.
54
+
55
+ === Private Blob Access
56
+
57
+ In the even that are using a Blob that has been configured for Private access, you will need to use the Shared Access Signature functionality of Azure. This functionality has been baked in to the `Attachment#expiring_url` method. Simply specify a time and a style and you will get a proper URL as follows:
58
+
59
+ object.attachment.expiring_url(30.minutes.since, :thumb)
60
+
61
+ For more information about Azure Shared Access Signatures, please refer to [here](http://azure.microsoft.com/en-us/documentation/articles/storage-dotnet-shared-access-signature-part-1/).
62
+
63
+ === Azure Environments
64
+
65
+ Microsoft offers specialized Azure implementations for special circumstances should the need arise. As of the most recent update of this gem, the AzureChinaCloud, AzureUSGovernment, and AzureGermanCloud environments all offer specific storage URL's that differ from those of the standard AzureCloud. These regions can be specified via the `:region` key of the `:azure_credentials` dictionary by using the symbols `:cn`, `:usgovt`, and `:de` respectively. When working with one of these environments, simply update your credentials to include the region as follows:
66
+
67
+ Paperclip::Attachment.default_options[:azure_credentials] = {
68
+ storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
69
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
70
+ container: ENV['AZURE_CONTAINER_NAME'],
71
+ region: :de
72
+ }
73
+
74
+ Or, in the instance where the credentials are specified at the model level:
75
+
76
+ has_attached_file :download,
77
+ storage: :azure,
78
+ azure_credentials: {
79
+ storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
80
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
81
+ container: ENV['AZURE_CONTAINER_NAME'],
82
+ region: :cn
83
+ }
84
+
85
+ === Managed identities (Entra ID)
86
+
87
+ To authenticate through managed identities instead of using a shared access key, omit passing the access key and pass the principal id of the identity instead. For example:
88
+
89
+ Paperclip::Attachment.default_options[:azure_credentials] = {
90
+ storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
91
+ pincipal_id: ENV['AZURE_STORAGE_PRINCIPAL_ID],
92
+ container: ENV['AZURE_CONTAINER_NAME'],
93
+ region: :de
94
+ }
95
+
96
+
97
+ == REQUIREMENTS:
98
+
99
+ * An Azure storage account.
100
+
101
+ == INSTALL:
102
+
103
+ Add this line to your application's Gemfile after the Paperclip gem:
104
+
105
+ gem 'md-paperclip-azure', '~> 2.0'
106
+
107
+ And then execute:
108
+
109
+ $ bundle install
110
+
111
+ == DEVELOPERS:
112
+
113
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
114
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
115
+ * Fork the project.
116
+ * After checking out the source, run:
117
+
118
+ $ rake newb
119
+
120
+ This task will install any missing dependencies, run the tests/specs, and generate the RDoc.
121
+ * Start a feature/bugfix branch.
122
+ * Commit and push until you are happy with your contribution.
123
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
124
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
125
+ * Submit a pull request for the finished product's integration.
126
+
127
+ == LICENSE:
128
+
129
+ (The MIT License)
130
+
131
+ Copyright (c) 2017 Supportify, Inc.
132
+ Copyright (c) 2023 Misty De Méo
133
+
134
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
135
+
136
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
137
+
138
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # -*- ruby -*-
2
+
3
+ require "rubygems"
4
+ require "hoe"
5
+
6
+ Hoe.plugin :bundler
7
+ Hoe.plugin :debug
8
+ Hoe.plugin :git
9
+ Hoe.plugin :gemspec
10
+ Hoe.plugin :rubygems
11
+
12
+ Hoe.spec "md-paperclip-azure" do
13
+ developer("Misty De Méo", "mistydemeo@gmail.com")
14
+ license "MIT" # this should match the license in the README
15
+
16
+ extra_deps << ['azure-storage-blob', '~> 2.0.1']
17
+ extra_deps << ['hashie', '~> 5.0']
18
+ extra_deps << ['addressable', '~> 2.5']
19
+
20
+ extra_dev_deps << ['kt-paperclip', '~> 7.1']
21
+ extra_dev_deps << ['sqlite3', '~> 1.3.8']
22
+ extra_dev_deps << ['rspec', '~> 3.0']
23
+ extra_dev_deps << ['activerecord', '~> 6.1']
24
+ extra_dev_deps << ['activerecord-import', '~> 1.4']
25
+ extra_dev_deps << ['activemodel', '~> 6.1']
26
+ extra_dev_deps << ['activesupport', '~> 6.1']
27
+ end
28
+
29
+ # vim: syntax=ruby
@@ -0,0 +1,5 @@
1
+ module Paperclip; end
2
+
3
+ class Paperclip::Azure
4
+ VERSION = "3.0.0"
5
+ end
@@ -0,0 +1,19 @@
1
+ module Paperclip
2
+ module Storage
3
+ module Azure
4
+ class Environment
5
+
6
+ ENVIRONMENT_SUFFIX = {
7
+ global: 'core.windows.net',
8
+ cn: 'core.chinacloudapi.cn',
9
+ de: "core.cloudapi.de",
10
+ usgovt: 'core.usgovcloudapi.net'
11
+ }
12
+
13
+ def self.url_for(account_name, region = nil)
14
+ "#{account_name}.blob.#{ENVIRONMENT_SUFFIX[region || :global]}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,271 @@
1
+ require "azure_blob"
2
+
3
+ module Paperclip
4
+ module Storage
5
+ # Azure's container file hosting service is a scalable, easy place to store files for
6
+ # distribution. You can find out more about it at http://azure.microsoft.com/en-us/services/storage/
7
+ #
8
+ # To use Paperclip with Azure, include the +azure-storage-blob+ gem in your Gemfile:
9
+ # gem 'azure-storage-blob'
10
+ # There are a few Azure-specific options for has_attached_file:
11
+ # * +azure_credentials+: Takes a path, a File, a Hash or a Proc. The path (or File) must point
12
+ # to a YAML file containing the +storage_access_key+ and +storage_account_name+ that azure
13
+ # gives you. You can 'environment-space' this just like you do to your
14
+ # database.yml file, so different environments can use different accounts:
15
+ # development:
16
+ # storage_account_name: foo
17
+ # storage_access_key: 123...
18
+ # test:
19
+ # storage_account_name: foo
20
+ # storage_access_key: abc...
21
+ # production:
22
+ # storage_account_name: foo
23
+ # storage_access_key: 456...
24
+ # This is not required, however, and the file may simply look like this:
25
+ # storage_account_name: foo
26
+ # storage_access_key: 456...
27
+ # In which case, those access keys will be used in all environments. You can also
28
+ # put your container name in this file, instead of adding it to the code directly.
29
+ # This is useful when you want the same account but a different container for
30
+ # development versus production.
31
+ # When using a Proc it provides a single parameter which is the attachment itself. A
32
+ # method #instance is available on the attachment which will take you back to your
33
+ # code. eg.
34
+ # class User
35
+ # has_attached_file :download,
36
+ # :storage => :azure,
37
+ # :azure_credentials => Proc.new{|a| a.instance.azure_credentials }
38
+ #
39
+ # def azure_credentials
40
+ # { :container => "xxx", :storage_account_name => "xxx", :storage_access_key => "xxx" }
41
+ # end
42
+ # end
43
+ #
44
+ # * +container+: This is the name of the Azure container that will store your files. Remember
45
+ # that the container must be unique across the storage account. If the container does not exist
46
+ # Paperclip will attempt to create it. The container name will not be interpolated.
47
+ # You can define the container as a Proc if you want to determine it's name at runtime.
48
+ # Paperclip will call that Proc with attachment as the only argument.
49
+ # * +path+: This is the key under the container in which the file will be stored. The
50
+ # URL will be constructed from the container and the path. This is what you will want
51
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
52
+ # Azure (strictly speaking) does not support directories, you can still use a / to
53
+ # separate parts of your file name.
54
+ # * +region+: Depending on the region, different base urls are used. Supported values :global, :de
55
+
56
+ module Azure
57
+ def self.extended base
58
+ base.instance_eval do
59
+ @azure_options = @options[:azure_options] || {}
60
+
61
+ unless @options[:url].to_s.match(/\A:azure.*url\z/) || @options[:url] == ":asset_host".freeze
62
+ @options[:path] = path_option.gsub(/:url/, @options[:url]).sub(/\A:rails_root\/public\/system/, "".freeze)
63
+ @options[:url] = ":azure_path_url".freeze
64
+ end
65
+ @options[:url] = @options[:url].inspect if @options[:url].is_a?(Symbol)
66
+
67
+ @http_proxy = @options[:http_proxy] || nil
68
+ end
69
+
70
+ unless Paperclip::Interpolations.respond_to? :azure_alias_url
71
+ Paperclip.interpolates(:azure_alias_url) do |attachment, style|
72
+ protocol = attachment.azure_protocol(style, true)
73
+ host = attachment.azure_host_alias
74
+ path = attachment.path(style).
75
+ split("/")[attachment.azure_prefixes_in_alias..-1].
76
+ join("/").
77
+ sub(%r{\A/}, "")
78
+ "#{protocol}//#{host}/#{path}"
79
+ end
80
+ end
81
+ Paperclip.interpolates(:azure_path_url) do |attachment, style|
82
+ attachment.azure_uri(style)
83
+ end unless Paperclip::Interpolations.respond_to? :azure_path_url
84
+ Paperclip.interpolates(:asset_host) do |attachment, style|
85
+ "#{attachment.path(style).sub(%r{\A/}, "".freeze)}"
86
+ end unless Paperclip::Interpolations.respond_to? :asset_host
87
+ end
88
+
89
+ def expiring_url(time = 3600, style_name = default_style)
90
+ if path(style_name)
91
+ obj_path = path(style_name).gsub(%r{\A/}, '')
92
+ expiry = Time.now.utc.advance(seconds: time).iso8601
93
+ azure_interface.signed_uri(obj_path, permissions: 'r', expiry:).to_s
94
+ else
95
+ url(style_name)
96
+ end
97
+ end
98
+
99
+ def azure_host_alias
100
+ @azure_host_alias = @options[:azure_host_alias]
101
+ @azure_host_alias = @azure_host_alias.call(self) if @azure_host_alias.respond_to?(:call)
102
+ @azure_host_alias
103
+ end
104
+
105
+ def azure_prefixes_in_alias
106
+ @azure_prefixes_in_alias ||= @options[:azure_prefixes_in_alias].to_i
107
+ end
108
+
109
+ def azure_protocol(style = default_style, with_colon = false)
110
+ protocol = @azure_options[:protocol]
111
+ protocol = protocol.call(style, self) if protocol.respond_to?(:call)
112
+
113
+ if with_colon && !protocol.empty?
114
+ "#{protocol}:"
115
+ else
116
+ protocol.to_s
117
+ end
118
+ end
119
+
120
+ def auto_connect_duration
121
+ @auto_connect_duration ||= @options[:auto_connect_duration] || azure_credentials[:auto_connect_duration] || 10
122
+ @auto_connect_duration
123
+ end
124
+
125
+ def azure_credentials
126
+ @azure_credentials ||= parse_credentials(@options[:azure_credentials])
127
+ end
128
+
129
+ def azure_account_name
130
+ account_name = @options[:azure_storage_account_name] || azure_credentials[:storage_account_name]
131
+ account_name = account_name.call(self) if account_name.is_a?(Proc)
132
+
133
+ account_name
134
+ end
135
+
136
+ def container_name
137
+ @container ||= @options[:container] || azure_credentials[:container]
138
+ @container = @container.call(self) if @container.respond_to?(:call)
139
+ @container or raise ArgumentError, "missing required :container option"
140
+ end
141
+
142
+ def azure_interface
143
+ @azure_interface ||= begin
144
+ config = {}
145
+
146
+ [:storage_account_name, :storage_access_key, :container, :principal_id, :use_managed_identities].each do |opt|
147
+ config[opt] = azure_credentials[opt] if azure_credentials[opt]
148
+ end
149
+
150
+ obtain_azure_instance_for(config.merge(@azure_options))
151
+ end
152
+ end
153
+
154
+ def obtain_azure_instance_for(options)
155
+ AzureBlob::Client.new(
156
+ account_name: options[:storage_account_name],
157
+ access_key: options[:storage_access_key],
158
+ principal_id: options[:principal_id],
159
+ use_managed_identities: options[:use_managed_identities],
160
+ container: container_name,
161
+ cloud_regions: azure_credentials[:region],
162
+ )
163
+ end
164
+
165
+ def azure_uri(style_name = default_style)
166
+ azure_interface.generate_uri("#{container_name}/#{path(style_name).gsub(%r{\A/}, '')}")
167
+ end
168
+
169
+ def azure_container
170
+ @azure_container ||= azure_interface.get_container_properties
171
+ end
172
+
173
+ def azure_object(style_name = default_style)
174
+ azure_interface.get_blob_properties path(style_name).sub(%r{\A/},'')
175
+ end
176
+
177
+ def parse_credentials(creds)
178
+ creds = creds.respond_to?('call') ? creds.call(self) : creds
179
+ creds = find_credentials(creds).stringify_keys
180
+ env = Object.const_defined?(:Rails) ? Rails.env : nil
181
+ (creds[env] || creds).symbolize_keys
182
+ end
183
+
184
+ def exists?(style = default_style)
185
+ if original_filename
186
+ azure_object(style).present?
187
+ else
188
+ false
189
+ end
190
+ rescue AzureBlob::Http::FileNotFoundError
191
+ false
192
+ end
193
+
194
+ def create_container
195
+ azure_interface.create_container container_name
196
+ end
197
+
198
+ def flush_writes #:nodoc:
199
+ @queued_for_write.each do |style, file|
200
+ begin
201
+ log("saving #{path(style)}")
202
+
203
+ write_options = {
204
+ content_type: file.content_type,
205
+ }
206
+
207
+ if azure_container
208
+ save_blob path(style).sub(%r{\A/},''), file, write_options
209
+ end
210
+ rescue AzureBlob::Http::FileNotFoundError
211
+ create_container
212
+ retry
213
+ ensure
214
+ file.rewind
215
+ end
216
+ end
217
+
218
+ after_flush_writes # allows attachment to clean up temp files
219
+
220
+ @queued_for_write = {}
221
+ end
222
+
223
+ def save_blob(storage_path, file, write_options)
224
+ write_options = write_options.merge(block_size: 4.megabytes) if file.size >= 64.megabytes
225
+ azure_interface.create_block_blob(storage_path, file, write_options)
226
+ end
227
+
228
+ def flush_deletes #:nodoc:
229
+ @queued_for_delete.each do |path|
230
+ begin
231
+ log("deleting #{path}")
232
+
233
+ azure_interface.delete_blob path
234
+ rescue AzureBlob::Http::FileNotFoundError
235
+ end
236
+ end
237
+ @queued_for_delete = []
238
+ end
239
+
240
+ def copy_to_local_file(style, local_dest_path)
241
+ log("copying #{path(style)} to local file #{local_dest_path}")
242
+
243
+ content = azure_interface.get_blob(path(style).sub(%r{\A/},''))
244
+
245
+ ::File.open(local_dest_path, 'wb') do |local_file|
246
+ local_file.write(content)
247
+ end
248
+ rescue AzureBlob::Http::FileNotFoundError
249
+ warn("Cannot copy #{path(style)} to local file #{local_dest_path}")
250
+ false
251
+ end
252
+
253
+ private
254
+
255
+ def find_credentials creds
256
+ case creds
257
+ when File
258
+ YAML::load(ERB.new(File.read(creds.path)).result)
259
+ when String, Pathname
260
+ YAML::load(ERB.new(File.read(creds)).result)
261
+ when Hash
262
+ creds
263
+ when NilClass
264
+ {}
265
+ else
266
+ raise ArgumentError, "Credentials given are not a path, file, proc, or hash."
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'paperclip', 'storage', 'azure')
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'paperclip/azure.rb'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "jd-paperclip-azure"
8
+ gem.version = Paperclip::Azure::VERSION
9
+ gem.authors = [ "Joé Dupuis" ]
10
+ gem.email = [ "joe@dupuis.io" ]
11
+
12
+ gem.summary = %q{Paperclip-Azure is a Paperclip storage driver for storing files in a Microsoft Azure Blob}
13
+ gem.homepage = "https://github.com/joedupuis/paperclip-azure"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.license = "MIT"
20
+
21
+
22
+ gem.add_dependency "azure-blob", "~> 0.5.2"
23
+ gem.add_dependency "hashie", "~> 5.0"
24
+ gem.add_dependency "addressable", "~> 2.5"
25
+
26
+ gem.add_development_dependency "kt-paperclip", "~> 7.1"
27
+ gem.add_development_dependency "sqlite3", "~> 1.3"
28
+ gem.add_development_dependency "rspec", "~> 3.0"
29
+ gem.add_development_dependency "activerecord", "~> 6.1"
30
+ gem.add_development_dependency "activerecord-import", "~> 1.4"
31
+ gem.add_development_dependency "activemodel", "~> 6.1"
32
+ gem.add_development_dependency "activesupport", "~> 6.1"
33
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'paperclip/storage/azure/environment'
3
+
4
+ describe 'Paperclip::Storage::Azure::Environment' do
5
+ subject { Paperclip::Storage::Azure::Environment }
6
+
7
+ describe '#url_for' do
8
+ let(:account_name) { 'foo' }
9
+
10
+ describe 'when the region is not supplied' do
11
+ it { expect(subject.url_for(account_name)).to eq("#{account_name}.blob.core.windows.net")}
12
+ end
13
+
14
+ describe 'when the region is China' do
15
+ it { expect(subject.url_for(account_name, :cn)).to eq("#{account_name}.blob.core.chinacloudapi.cn")}
16
+ end
17
+
18
+ describe 'when the region is Germany' do
19
+ it { expect(subject.url_for(account_name, :de)).to eq("#{account_name}.blob.core.cloudapi.de")}
20
+ end
21
+
22
+ describe 'when the region is the US Govt' do
23
+ it { expect(subject.url_for(account_name, :usgovt)).to eq("#{account_name}.blob.core.usgovcloudapi.net")}
24
+ end
25
+ end
26
+ end