paperclip-azure 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Manifest.txt +27 -1
  3. data/README.txt +25 -7
  4. data/Rakefile +7 -1
  5. data/lib/paperclip-azure.rb +0 -1
  6. data/lib/paperclip/azure.rb +1 -1
  7. data/lib/paperclip/storage/azure.rb +43 -57
  8. data/lib/paperclip/storage/azure/environment.rb +2 -2
  9. data/spec/database.yml +3 -0
  10. data/spec/paperclip/storage/azure_spec.rb +435 -0
  11. data/spec/spec_helper.rb +29 -3
  12. data/spec/support/fake_model.rb +25 -0
  13. data/spec/support/fake_rails.rb +12 -0
  14. data/spec/support/fixtures/12k.png +0 -0
  15. data/spec/support/fixtures/50x50.png +0 -0
  16. data/spec/support/fixtures/5k.png +0 -0
  17. data/spec/support/fixtures/animated +0 -0
  18. data/spec/support/fixtures/animated.gif +0 -0
  19. data/spec/support/fixtures/animated.unknown +0 -0
  20. data/spec/support/fixtures/azure.yml +8 -0
  21. data/spec/support/fixtures/bad.png +1 -0
  22. data/spec/support/fixtures/empty.html +1 -0
  23. data/spec/support/fixtures/empty.xlsx +0 -0
  24. data/spec/support/fixtures/fog.yml +8 -0
  25. data/spec/support/fixtures/rotated.jpg +0 -0
  26. data/spec/support/fixtures/spaced file.jpg +0 -0
  27. data/spec/support/fixtures/spaced file.png +0 -0
  28. data/spec/support/fixtures/text.txt +1 -0
  29. data/spec/support/fixtures/twopage.pdf +0 -0
  30. data/spec/support/fixtures/uppercase.PNG +0 -0
  31. data/spec/support/mock_attachment.rb +24 -0
  32. data/spec/support/mock_interpolator.rb +24 -0
  33. data/spec/support/mock_url_generator_builder.rb +27 -0
  34. data/spec/support/model_reconstruction.rb +68 -0
  35. data/spec/support/test_data.rb +13 -0
  36. data/spec/support/version_helper.rb +9 -0
  37. metadata +116 -6
  38. data/lib/azure/core/auth/shared_access_signature.rb +0 -84
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a233ac7477aced4158a50fbed0ed87a16be96cc9
4
- data.tar.gz: f2fd4c35c33316c5fa8da7bae2e2506b0d8390f5
3
+ metadata.gz: 911cdf6bd98931b2d25cb0dc5db1f0ce60b0bb9a
4
+ data.tar.gz: bc3c27455a1b7f9aba2e217eed8a8d245a2c1e55
5
5
  SHA512:
6
- metadata.gz: bbf61c97fadc054badbb6f946502458303af5dde02c628ee0dd70acae763799c58b6925b88a8d5e76c59c7fb9e6ed23fe023ecc121abfc2af40e23aa16f2180b
7
- data.tar.gz: edaee95e6fbd6a7965fadfc4964d69b696025bdeedc4c3af6d6d12b64d604742c169787dbf6ddbf277c0a3ef9da8b276d45488f26941666f69e37e72b14668da
6
+ metadata.gz: 858ac4cd36c47aa4bd052ed076af97357f57e407f25815ff0804ab5da2976e78200e749d5290d20a53e34397c51355617f8ef62fe608195fa5b0c5eb472b1648
7
+ data.tar.gz: 08744fa9bfadc893045c65966b35ddddb3dc3318490fe15f08771432d1d5904022e85d4a6aa6c72db814dbb0836cf75df2989a8ae55f6c8f4f6be62b9a6e31dc
data/Manifest.txt CHANGED
@@ -3,10 +3,36 @@ History.txt
3
3
  Manifest.txt
4
4
  README.txt
5
5
  Rakefile
6
- lib/azure/core/auth/shared_access_signature.rb
7
6
  lib/paperclip/storage/azure/environment.rb
8
7
  lib/paperclip/storage/azure.rb
9
8
  lib/paperclip/azure.rb
10
9
  lib/paperclip-azure.rb
11
10
  spec/paperclip/storage/azure/environment_spec.rb
11
+ spec/paperclip/storage/azure_spec.rb
12
+ spec/support/fixtures/12k.png
13
+ spec/support/fixtures/50x50.png
14
+ spec/support/fixtures/5k.png
15
+ spec/support/fixtures/animated
16
+ spec/support/fixtures/animated.gif
17
+ spec/support/fixtures/animated.unknown
18
+ spec/support/fixtures/azure.yml
19
+ spec/support/fixtures/bad.png
20
+ spec/support/fixtures/empty.html
21
+ spec/support/fixtures/empty.xlsx
22
+ spec/support/fixtures/fog.yml
23
+ spec/support/fixtures/rotated.jpg
24
+ spec/support/fixtures/spaced file.jpg
25
+ spec/support/fixtures/spaced file.png
26
+ spec/support/fixtures/text.txt
27
+ spec/support/fixtures/twopage.pdf
28
+ spec/support/fixtures/uppercase.PNG
29
+ spec/support/fake_model.rb
30
+ spec/support/fake_rails.rb
31
+ spec/support/mock_attachment.rb
32
+ spec/support/mock_interpolator.rb
33
+ spec/support/mock_url_generator_builder.rb
34
+ spec/support/model_reconstruction.rb
35
+ spec/support/test_data.rb
36
+ spec/support/version_helper.rb
37
+ spec/database.yml
12
38
  spec/spec_helper.rb
data/README.txt CHANGED
@@ -18,12 +18,11 @@ Paperclip-Azure is a [Paperclip](https://github.com/thoughtbot/paperclip) storag
18
18
  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:
19
19
 
20
20
  Paperclip::Attachment.default_options[:storage] = :azure
21
- Paperclip::Attachment.default_options[:url] = ':azure_path_url'
22
- Paperclip::Attachment.default_options[:path] = ":class/:attachment/:id/:style/:filename"
23
- Paperclip::Attachment.default_options[:storage] = :azure
21
+ Paperclip::Attachment.default_options[:url] = ':azure_path_url'
22
+ Paperclip::Attachment.default_options[:path] = ':class/:attachment/:id/:style/:filename'
24
23
  Paperclip::Attachment.default_options[:azure_credentials] = {
25
24
  storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
26
- access_key: ENV['AZURE_ACCESS_KEY'],
25
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
27
26
  container: ENV['AZURE_CONTAINER_NAME']
28
27
  }
29
28
 
@@ -33,10 +32,29 @@ Or, at the level of the model such as in the following example:
33
32
  storage: :azure,
34
33
  azure_credentials: {
35
34
  storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
36
- access_key: ENV['AZURE_ACCESS_KEY'],
35
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
37
36
  container: ENV['AZURE_CONTAINER_NAME']
38
37
  }
39
38
 
39
+ 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:
40
+
41
+ development:
42
+ storage_account_name: foo
43
+ storage_access_key: 123...
44
+ test:
45
+ storage_account_name: foo
46
+ storage_access_key: abc...
47
+ production:
48
+ storage_account_name: foo
49
+ storage_access_key: 456...
50
+
51
+ This is not required, however, and the file may simply look like this:
52
+
53
+ storage_account_name: foo
54
+ storage_access_key: 456...
55
+
56
+ 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.
57
+
40
58
  === Private Blob Access
41
59
 
42
60
  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:
@@ -51,7 +69,7 @@ Microsoft offers specialized Azure implementations for special circumstances sho
51
69
 
52
70
  Paperclip::Attachment.default_options[:azure_credentials] = {
53
71
  storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
54
- access_key: ENV['AZURE_ACCESS_KEY'],
72
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
55
73
  container: ENV['AZURE_CONTAINER_NAME'],
56
74
  region: :de
57
75
  }
@@ -62,7 +80,7 @@ Or, in the instance where the credentials are specified at the model level:
62
80
  storage: :azure,
63
81
  azure_credentials: {
64
82
  storage_account_name: ENV['AZURE_STORAGE_ACCOUNT'],
65
- access_key: ENV['AZURE_ACCESS_KEY'],
83
+ storage_access_key: ENV['AZURE_STORAGE_ACCESS_KEY'],
66
84
  container: ENV['AZURE_CONTAINER_NAME'],
67
85
  region: :cn
68
86
  }
data/Rakefile CHANGED
@@ -13,12 +13,18 @@ Hoe.spec "paperclip-azure" do
13
13
  developer("hireross.com", "help@hireross.com")
14
14
  license "MIT" # this should match the license in the README
15
15
 
16
- extra_deps << ['azure', '~> 0.7']
16
+ extra_deps << ['azure-storage', '~> 0.12']
17
17
  extra_deps << ['hashie', '~> 3.5']
18
18
  extra_deps << ['addressable', '~> 2.5']
19
19
 
20
+ extra_dev_deps << ['paperclip', '>= 4.3.6']
21
+ extra_dev_deps << ['sqlite3', '~> 1.3.8']
20
22
  extra_dev_deps << ['rspec', '~> 3.0']
21
23
  extra_dev_deps << ['simplecov', '~> 0.14']
24
+ extra_dev_deps << ['activerecord', '>= 4.2.0']
25
+ extra_dev_deps << ['activerecord-import', '~> 0.19']
26
+ extra_dev_deps << ['activemodel', '>= 4.2.0']
27
+ extra_dev_deps << ['activesupport', '>= 4.2.0']
22
28
  end
23
29
 
24
30
  # vim: syntax=ruby
@@ -1,6 +1,5 @@
1
1
  require 'azure'
2
2
 
3
- require File.join(File.dirname(__FILE__), 'azure', 'core', 'auth', 'shared_access_signature')
4
3
  require File.join(File.dirname(__FILE__), 'paperclip', 'storage', 'azure')
5
4
 
6
5
  module Azure
@@ -1,5 +1,5 @@
1
1
  module Paperclip; end
2
2
 
3
3
  class Paperclip::Azure
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
@@ -1,3 +1,4 @@
1
+ require 'azure/storage'
1
2
  require 'paperclip/storage/azure/environment'
2
3
 
3
4
  module Paperclip
@@ -5,25 +6,23 @@ module Paperclip
5
6
  # Azure's container file hosting service is a scalable, easy place to store files for
6
7
  # distribution. You can find out more about it at http://azure.microsoft.com/en-us/services/storage/
7
8
  #
8
- # To use Paperclip with Azure, include the +azure+ gem in your Gemfile:
9
- # gem 'azure'
10
9
  # There are a few Azure-specific options for has_attached_file:
11
10
  # * +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 +access_key+ and +storage_account+ that azure
11
+ # to a YAML file containing the +storage_access_key+ and +storage_account_name+ that azure
13
12
  # gives you. You can 'environment-space' this just like you do to your
14
13
  # database.yml file, so different environments can use different accounts:
15
14
  # development:
16
15
  # storage_account_name: foo
17
- # access_key: 123...
16
+ # storage_access_key: 123...
18
17
  # test:
19
18
  # storage_account_name: foo
20
- # access_key: abc...
19
+ # storage_access_key: abc...
21
20
  # production:
22
21
  # storage_account_name: foo
23
- # access_key: 456...
22
+ # storage_access_key: 456...
24
23
  # This is not required, however, and the file may simply look like this:
25
24
  # storage_account_name: foo
26
- # access_key: 456...
25
+ # storage_access_key: 456...
27
26
  # In which case, those access keys will be used in all environments. You can also
28
27
  # put your container name in this file, instead of adding it to the code directly.
29
28
  # This is useful when you want the same account but a different container for
@@ -37,7 +36,7 @@ module Paperclip
37
36
  # :azure_credentials => Proc.new{|a| a.instance.azure_credentials }
38
37
  #
39
38
  # def azure_credentials
40
- # { :container => "xxx", :storage_account_name => "xxx", :access_key => "xxx" }
39
+ # { :container => "xxx", :storage_account_name => "xxx", :storage_access_key => "xxx" }
41
40
  # end
42
41
  # end
43
42
  #
@@ -64,26 +63,35 @@ module Paperclip
64
63
 
65
64
  base.instance_eval do
66
65
  @azure_options = @options[:azure_options] || {}
66
+
67
+ unless @options[:url].to_s.match(/\A:azure.*url\z/) || @options[:url] == ":asset_host".freeze
68
+ @options[:path] = path_option.gsub(/:url/, @options[:url]).sub(/\A:rails_root\/public\/system/, "".freeze)
69
+ @options[:url] = ":azure_path_url".freeze
70
+ end
71
+ @options[:url] = @options[:url].inspect if @options[:url].is_a?(Symbol)
72
+
73
+ @http_proxy = @options[:http_proxy] || nil
67
74
  end
68
75
 
69
76
  Paperclip.interpolates(:azure_path_url) do |attachment, style|
70
77
  attachment.azure_uri(style)
71
78
  end unless Paperclip::Interpolations.respond_to? :azure_path_url
79
+ Paperclip.interpolates(:asset_host) do |attachment, style|
80
+ "#{attachment.path(style).sub(%r{\A/}, "".freeze)}"
81
+ end unless Paperclip::Interpolations.respond_to? :asset_host
72
82
  end
73
83
 
74
84
  def expiring_url(time = 3600, style_name = default_style)
75
85
  if path(style_name)
76
- uri = azure_uri(style_name)
77
- signer = ::Azure::Core::Auth::SharedAccessSignature.new(uri, {
78
- resource: 'b',
79
- permissions: 'r',
80
- start: 5.minutes.ago.utc.iso8601,
81
- expiry: time.since.utc.iso8601,
82
- access_key: azure_credentials[:access_key]
83
- },
84
- azure_account_name
85
- )
86
- signer.sign
86
+ uri = URI azure_uri(style_name)
87
+ generator = ::Azure::Storage::Core::Auth::SharedAccessSignature.new azure_account_name,
88
+ azure_credentials[:storage_access_key]
89
+
90
+ generator.signed_uri uri, false, service: 'b',
91
+ resource: 'b',
92
+ permissions: 'r',
93
+ start: (Time.now - (5 * 60)).utc.iso8601,
94
+ expiry: (Time.now + time).utc.iso8601
87
95
  else
88
96
  url(style_name)
89
97
  end
@@ -115,7 +123,7 @@ module Paperclip
115
123
  @azure_interface ||= begin
116
124
  config = {}
117
125
 
118
- [:storage_account_name, :access_key, :container].each do |opt|
126
+ [:storage_account_name, :storage_access_key, :container].each do |opt|
119
127
  config[opt] = azure_credentials[opt] if azure_credentials[opt]
120
128
  end
121
129
 
@@ -123,54 +131,32 @@ module Paperclip
123
131
  end
124
132
  end
125
133
 
126
- def obtain_azure_instance_for(options)
127
- instances = (Thread.current[:paperclip_azure_instances] ||= {})
134
+ def azure_storage_client
135
+ config = {}
128
136
 
129
- unless instances[options]
130
- signer = ::Azure::Core::Auth::SharedKey.new options[:storage_account_name], options[:access_key]
131
- service = ::Azure::BlobService.new(signer, options[:storage_account_name])
132
-
133
- require 'azure/core/http/retry_policy' # For Some Reason, All Other Loading Locations Fail
134
- service.filters << ::Azure::Core::Http::RetryPolicy.new do |response, retry_data|
135
- status_code = case
136
- when !response.nil?
137
- response.status_code
138
- when !retry_data[:error].nil?
139
- retry_data[:error].status_code
140
- else
141
- 500
142
- end
143
- status_code = 500 if status_code == 0
144
- retry_data[:count] ||= 0
145
-
146
- if (!response.nil? && response.success? && retry_data[:error].nil?) ||
147
- (status_code >= 300 && status_code < 500 && status_code != 408) ||
148
- status_code == 501 ||
149
- status_code == 505 ||
150
- (!retry_data[:error].nil? && retry_data[:error].description == 'Blob type of the blob reference doesn\'t match blob type of the blob.') ||
151
- retry_data[:count] >= 5
152
- retry_data[:count] = 0
153
- else
154
- retry_data[:count] += 1
137
+ [:storage_account_name, :storage_access_key].each do |opt|
138
+ config[opt] = azure_credentials[opt] if azure_credentials[opt]
139
+ end
155
140
 
156
- sleep (retry_data[:count] - 1) * 5
157
- end
141
+ @azure_storage_client ||= ::Azure::Storage::Client.create config
142
+ end
158
143
 
159
- retry_data[:count] > 0
160
- end
144
+ def obtain_azure_instance_for(options)
145
+ instances = (Thread.current[:paperclip_azure_instances] ||= {})
146
+ return instances[options] if instance[options]
161
147
 
162
- instances[options] = service
163
- end
148
+ service = ::Azure::Storage::Blob::BlobService.new(client: azure_storage_client)
149
+ service.with_filter ::Azure::Storage::Core::Filter::ExponentialRetryPolicyFilter.new
164
150
 
165
- instances[options]
151
+ instances[options] = service
166
152
  end
167
153
 
168
154
  def azure_uri(style_name = default_style)
169
- "#{azure_base_url}/#{container_name}/#{path(style_name).gsub(%r{\A/}, '')}"
155
+ "https://#{azure_base_url}/#{container_name}/#{path(style_name).gsub(%r{\A/}, '')}"
170
156
  end
171
157
 
172
158
  def azure_base_url
173
- Environment.url_for azure_account_name, @azure_credentials[:region]
159
+ Environment.url_for azure_account_name, azure_credentials[:region]
174
160
  end
175
161
 
176
162
  def azure_container
@@ -10,8 +10,8 @@ module Paperclip
10
10
  usgovt: 'core.usgovcloudapi.net'
11
11
  }
12
12
 
13
- def self.url_for(account_name, region = :global)
14
- "#{account_name}.blob.#{ENVIRONMENT_SUFFIX[region]}"
13
+ def self.url_for(account_name, region = nil)
14
+ "#{account_name}.blob.#{ENVIRONMENT_SUFFIX[region || :global]}"
15
15
  end
16
16
  end
17
17
  end
data/spec/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,435 @@
1
+ require 'spec_helper'
2
+ require "base64"
3
+
4
+ describe Paperclip::Storage::Azure do
5
+ let(:storage_access_key) { 'kiaY4+GkLMVxnfOK2X+eCJOE06J8QtHC6XNuXVwt8Pp4kMezYaa7cNjtYnZr4/b732RKdz5pZwl8RN9yb8gBCg==' }
6
+
7
+ describe "#parse_credentials" do
8
+ let(:credentials) {{
9
+ 'production' => {key: '12345'},
10
+ development: {key: "54321"}
11
+ }}
12
+
13
+ before do
14
+ @proxy_settings = {host: "127.0.0.1", port: 8888, user: "foo", password: "bar"}
15
+ rebuild_model storage: :azure,
16
+ container: "testing",
17
+ azure_credentials: {not: :important}
18
+ @dummy = Dummy.new
19
+ @avatar = @dummy.avatar
20
+ end
21
+
22
+ it "gets the correct credentials when RAILS_ENV is production" do
23
+ rails_env("production") do
24
+ expect(@avatar.parse_credentials(credentials)).to eq({key: "12345"})
25
+ end
26
+ end
27
+
28
+ it "gets the correct credentials when RAILS_ENV is development" do
29
+ rails_env("development") do
30
+ expect(@avatar.parse_credentials(credentials)).to eq({key: "54321"})
31
+ end
32
+ end
33
+
34
+ it "returns the argument if the key does not exist" do
35
+ rails_env("not really an env") do
36
+ expect(@avatar.parse_credentials(test: "12345")).to eq({test: "12345"})
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ describe '#container_name' do
43
+ describe ":container option via :azure_credentials" do
44
+ before do
45
+ rebuild_model storage: :azure,
46
+ azure_credentials: {container: 'testing'}
47
+ @dummy = Dummy.new
48
+ end
49
+
50
+ it "populates #container_name" do
51
+ expect(@dummy.avatar.container_name).to eq('testing')
52
+ end
53
+ end
54
+
55
+ describe ":container option" do
56
+ before do
57
+ rebuild_model storage: :azure,
58
+ container: "testing",
59
+ azure_credentials: {}
60
+ @dummy = Dummy.new
61
+ end
62
+
63
+ it "populates #container_name" do
64
+ expect(@dummy.avatar.container_name).to eq('testing')
65
+ end
66
+ end
67
+
68
+ describe "missing :container option" do
69
+ before do
70
+ rebuild_model storage: :azure,
71
+ azure_credentials: {not: :important}
72
+
73
+ @dummy = Dummy.new
74
+ @dummy.avatar = stringy_file
75
+ end
76
+
77
+ it "raises an argument error" do
78
+ expect{ @dummy.avatar.container_name }.to raise_error(ArgumentError, /missing required :container option/)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "" do
84
+ before do
85
+ rebuild_model storage: :azure,
86
+ azure_credentials: {
87
+ storage_account_name: 'storage',
88
+ storage_access_key: storage_access_key
89
+ },
90
+ container: "container",
91
+ path: ":attachment/:basename:dotextension",
92
+ url: ":azure_path_url"
93
+
94
+ @dummy = Dummy.new
95
+ @dummy.avatar = stringy_file
96
+
97
+ allow(@dummy).to receive(:new_record?).and_return(false)
98
+ end
99
+
100
+ it "returns urls based on Azure paths" do
101
+ expect(@dummy.avatar.url).to match(%r{^https://storage.blob.core.windows.net/container/avatars/data[^\.]})
102
+ end
103
+ end
104
+
105
+ describe "An attachment that uses Azure for storage and has styles that return different file types" do
106
+ before do
107
+ rebuild_model storage: :azure,
108
+ styles: { large: ['500x500#', :jpg] },
109
+ container: "container",
110
+ path: ":attachment/:basename:dotextension",
111
+ azure_credentials: {
112
+ 'access_key_id' => "12345",
113
+ 'secret_access_key' => "54321"
114
+ }
115
+
116
+ File.open(fixture_file('5k.png'), 'rb') do |file|
117
+ @dummy = Dummy.new
118
+ @dummy.avatar = file
119
+
120
+ allow(@dummy).to receive(:new_record?).and_return(false)
121
+ end
122
+ end
123
+
124
+ it "returns a url containing the correct original file mime type" do
125
+ expect(@dummy.avatar.url).to match(/.+\/5k.png/)
126
+ end
127
+
128
+ it "returns a url containing the correct processed file mime type" do
129
+ expect(@dummy.avatar.url(:large)).to match(/.+\/5k.jpg/)
130
+ end
131
+ end
132
+
133
+ describe "An attachment that uses Azure for storage and has spaces in file name" do
134
+ before do
135
+ rebuild_model storage: :azure,
136
+ styles: { large: ["500x500#", :jpg] },
137
+ container: "container",
138
+ azure_credentials: {
139
+ "storage_access_key" => "54321"
140
+ }
141
+
142
+ File.open(fixture_file("spaced file.png"), "rb") do |file|
143
+ @dummy = Dummy.new
144
+ @dummy.avatar = file
145
+
146
+ allow(@dummy).to receive(:new_record?).and_return(false)
147
+ end
148
+ end
149
+
150
+ it "returns a replaced version for path" do
151
+ expect(@dummy.avatar.path).to match(/.+\/spaced_file\.png/)
152
+ end
153
+
154
+ it "returns a replaced version for url" do
155
+ expect(@dummy.avatar.url).to match(/.+\/spaced_file\.png/)
156
+ end
157
+ end
158
+
159
+ describe "An attachment that uses Azure for storage and has a question mark in file name" do
160
+ before do
161
+ rebuild_model storage: :azure,
162
+ styles: { large: ['500x500#', :jpg] },
163
+ container: "container",
164
+ azure_credentials: {
165
+ 'storage_access_key' => "54321"
166
+ }
167
+
168
+ stringio = stringy_file
169
+ class << stringio
170
+ def original_filename
171
+ "question?mark.png"
172
+ end
173
+ end
174
+
175
+ file = Paperclip.io_adapters.for(stringio)
176
+
177
+ @dummy = Dummy.new
178
+ @dummy.avatar = file
179
+ @dummy.save
180
+
181
+ allow(@dummy).to receive(:new_record?).and_return(false)
182
+ end
183
+
184
+ it "returns a replaced version for path" do
185
+ expect(@dummy.avatar.path).to match(/.+\/question_mark\.png/)
186
+ end
187
+
188
+ it "returns a replaced version for url" do
189
+ expect(@dummy.avatar.url).to match(/.+\/question_mark\.png/)
190
+ end
191
+ end
192
+
193
+ describe ":asset_host path Interpolations" do
194
+ before do
195
+ rebuild_model storage: :azure,
196
+ azure_credentials: {},
197
+ container: "container",
198
+ path: ":attachment/:basename:dotextension",
199
+ url: ":asset_host"
200
+ @dummy = Dummy.new
201
+ @dummy.avatar = stringy_file
202
+ allow(@dummy).to receive(:new_record?).and_return(false)
203
+ end
204
+
205
+ it "returns a relative URL for Rails to calculate assets host" do
206
+ expect(@dummy.avatar.url).to match(%r{^avatars/data[^\.]})
207
+ end
208
+ end
209
+
210
+ describe "#expiring_url" do
211
+ before { @dummy = Dummy.new }
212
+
213
+ describe "with no attachment" do
214
+ before { expect(@dummy.avatar.exists?).to be_falsey }
215
+
216
+ it "returns the default URL" do
217
+ expect(@dummy.avatar.expiring_url).to eq(@dummy.avatar.url)
218
+ end
219
+
220
+ it 'generates a url for a style when a file does not exist' do
221
+ expect(@dummy.avatar.expiring_url(3600, :thumb)).to eq(@dummy.avatar.url(:thumb))
222
+ end
223
+ end
224
+ end
225
+
226
+ describe "Generating a url with an expiration for each style" do
227
+ before do
228
+ rebuild_model storage: :azure,
229
+ azure_credentials: {
230
+ production: {
231
+ storage_account_name: 'prod_storage',
232
+ storage_access_key: 'YWNjZXNzLWtleQ==',
233
+ container: "prod_container"
234
+ },
235
+ development: {
236
+ storage_account_name: 'dev_storage',
237
+ storage_access_key: 'YWNjZXNzLWtleQ==',
238
+ container: "dev_container"
239
+ }
240
+ },
241
+ path: ":attachment/:style/:basename:dotextension"
242
+
243
+ rails_env("production") do
244
+ @dummy = Dummy.new
245
+ @dummy.avatar = stringy_file
246
+ end
247
+
248
+ allow(::Azure::Storage::Core::Auth::SharedAccessSignature).to receive(:new).and_call_original
249
+ allow(::Azure::Storage::Core::Auth::SharedAccessSignatureSigner).to receive(:new).and_call_original
250
+ end
251
+
252
+ it "generates a url for the thumb" do
253
+ rails_env("production") do
254
+ expect { @dummy.avatar.expiring_url(1800, :thumb) }.not_to raise_error
255
+ end
256
+
257
+ expect(::Azure::Storage::Core::Auth::SharedAccessSignature).to have_received(:new)
258
+ .with('prod_storage', anything)
259
+ end
260
+
261
+ it "generates a url for the default style" do
262
+ rails_env("production") do
263
+ expect { @dummy.avatar.expiring_url(1800) }.not_to raise_error
264
+ end
265
+
266
+ expect(::Azure::Storage::Core::Auth::SharedAccessSignature).to have_received(:new)
267
+ .with('prod_storage', anything)
268
+ end
269
+ end
270
+
271
+ context "Parsing Azure credentials with a container in them" do
272
+ before do
273
+ rebuild_model storage: :azure,
274
+ azure_credentials: {
275
+ production: { container: "prod_container" },
276
+ development: { container: "dev_container" }
277
+ }
278
+ @dummy = Dummy.new
279
+ end
280
+
281
+ it "gets the right container in production" do
282
+ rails_env("production") do
283
+ expect(@dummy.avatar.container_name).to eq("prod_container")
284
+ end
285
+ end
286
+
287
+ it "gets the right container in development" do
288
+ rails_env("development") do
289
+ expect(@dummy.avatar.container_name).to eq("dev_container")
290
+ end
291
+ end
292
+ end
293
+
294
+ context "An attachment with Azure storage" do
295
+ before do
296
+ rebuild_model storage: :azure,
297
+ container: "testing",
298
+ path: ":attachment/:style/:basename:dotextension",
299
+ azure_credentials: {
300
+ storage_account_name: 'storage',
301
+ storage_access_key: storage_access_key
302
+ }
303
+ end
304
+
305
+ it "is extended by the Azure module" do
306
+ expect(Dummy.new.avatar).to be_a(Paperclip::Storage::Azure)
307
+ end
308
+
309
+ it "won't be extended by the Filesystem module" do
310
+ expect(Dummy.new.avatar).not_to be_a(Paperclip::Storage::Filesystem)
311
+ end
312
+ end
313
+
314
+ describe "An attachment with Azure storage and container defined as a Proc" do
315
+ before do
316
+ rebuild_model storage: :azure,
317
+ container: lambda { |attachment| "container_#{attachment.instance.other}" },
318
+ azure_credentials: {not: :important}
319
+ end
320
+
321
+ it "gets the right container name" do
322
+ expect(Dummy.new(other: 'a').avatar.container_name).to eq("container_a")
323
+ expect(Dummy.new(other: 'b').avatar.container_name).to eq("container_b")
324
+ end
325
+ end
326
+
327
+ context "An attachment with Azure storage and Azure credentials defined as a Proc" do
328
+ before do
329
+ rebuild_model storage: :azure,
330
+ container: {not: :important},
331
+ azure_credentials: lambda { |attachment|
332
+ Hash['storage_access_key' => "secret#{attachment.instance.other}"]
333
+ }
334
+ end
335
+
336
+ it "gets the right credentials" do
337
+ expect(Dummy.new(other: '1234').avatar.azure_credentials[:storage_access_key]).to eq("secret1234")
338
+ end
339
+ end
340
+
341
+ context "An attachment with Azure storage and Azure credentials in an unsupported manor" do
342
+ before do
343
+ rebuild_model storage: :azure,
344
+ container: "testing",
345
+ azure_credentials: ["unsupported"]
346
+ @dummy = Dummy.new
347
+ end
348
+
349
+ it "does not accept the credentials" do
350
+ expect { @dummy.avatar.azure_credentials }.to raise_error(ArgumentError)
351
+ end
352
+ end
353
+
354
+ context "An attachment with Azure storage and Azure credentials not supplied" do
355
+ before do
356
+ rebuild_model storage: :azure, container: "testing"
357
+ @dummy = Dummy.new
358
+ end
359
+
360
+ it "does not parse any credentials" do
361
+ expect(@dummy.avatar.azure_credentials).to eq({})
362
+ end
363
+ end
364
+
365
+ describe "with Azure credentials supplied as Pathname" do
366
+ before do
367
+ ENV['AZURE_CONTAINER'] = 'pathname_container'
368
+ ENV['AZURE_STORAGE_ACCOUNT'] = 'pathname_storage_account'
369
+ ENV['AZURE_STORAGE_ACCESS_KEY'] = storage_access_key
370
+
371
+ rails_env('test') do
372
+ rebuild_model storage: :azure,
373
+ azure_credentials: Pathname.new(fixture_file('azure.yml'))
374
+
375
+ Dummy.delete_all
376
+ @dummy = Dummy.new
377
+ end
378
+ end
379
+
380
+ it "parses the credentials" do
381
+ expect(@dummy.avatar.container_name).to eq('pathname_container')
382
+ end
383
+ end
384
+
385
+ describe "with Azure credentials in a YAML file" do
386
+ before do
387
+ ENV['AZURE_CONTAINER'] = 'pathname_container'
388
+ ENV['AZURE_STORAGE_ACCOUNT'] = 'pathname_storage_account'
389
+ ENV['AZURE_STORAGE_ACCESS_KEY'] = storage_access_key
390
+
391
+ rails_env('test') do
392
+ rebuild_model storage: :azure,
393
+ azure_credentials: File.new(fixture_file('azure.yml'))
394
+
395
+ Dummy.delete_all
396
+
397
+ @dummy = Dummy.new
398
+ end
399
+ end
400
+
401
+ it "runs the file through ERB" do
402
+ expect(@dummy.avatar.container_name).to eq('pathname_container')
403
+ end
404
+ end
405
+
406
+ describe "path is a proc" do
407
+ before do
408
+ rebuild_model storage: :azure,
409
+ path: ->(attachment) { attachment.instance.attachment_path }
410
+
411
+ @dummy = Dummy.new
412
+ @dummy.class_eval do
413
+ def attachment_path
414
+ '/some/dynamic/path'
415
+ end
416
+ end
417
+ @dummy.avatar = stringy_file
418
+ end
419
+
420
+ it "returns a correct path" do
421
+ expect(@dummy.avatar.path).to eq('/some/dynamic/path')
422
+ end
423
+ end
424
+
425
+ private
426
+
427
+ def rails_env(env)
428
+ stored_env, Rails.env = Rails.env, env
429
+ begin
430
+ yield
431
+ ensure
432
+ Rails.env = stored_env
433
+ end
434
+ end
435
+ end