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
@@ -0,0 +1,426 @@
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
+ 'storage_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, Paperclip::Attachment.default_options[:adapter_options])
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
+ end
248
+
249
+ it "generates a url for the thumb" do
250
+ rails_env("production") do
251
+ expect(@dummy.avatar.expiring_url(1800, :thumb)).to include("https://prod_storage.blob.core.windows.net/prod_container/avatars/thumb/data")
252
+ end
253
+ end
254
+
255
+ it "generates a url for the default style" do
256
+ rails_env("production") do
257
+ expect(@dummy.avatar.expiring_url(1800)).to include("https://prod_storage.blob.core.windows.net/prod_container/avatars/original/data")
258
+ end
259
+ end
260
+ end
261
+
262
+ context "Parsing Azure credentials with a container in them" do
263
+ before do
264
+ rebuild_model storage: :azure,
265
+ azure_credentials: {
266
+ production: { container: "prod_container" },
267
+ development: { container: "dev_container" }
268
+ }
269
+ @dummy = Dummy.new
270
+ end
271
+
272
+ it "gets the right container in production" do
273
+ rails_env("production") do
274
+ expect(@dummy.avatar.container_name).to eq("prod_container")
275
+ end
276
+ end
277
+
278
+ it "gets the right container in development" do
279
+ rails_env("development") do
280
+ expect(@dummy.avatar.container_name).to eq("dev_container")
281
+ end
282
+ end
283
+ end
284
+
285
+ context "An attachment with Azure storage" do
286
+ before do
287
+ rebuild_model storage: :azure,
288
+ container: "testing",
289
+ path: ":attachment/:style/:basename:dotextension",
290
+ azure_credentials: {
291
+ storage_account_name: 'storage',
292
+ storage_access_key: storage_access_key
293
+ }
294
+ end
295
+
296
+ it "is extended by the Azure module" do
297
+ expect(Dummy.new.avatar).to be_a(Paperclip::Storage::Azure)
298
+ end
299
+
300
+ it "won't be extended by the Filesystem module" do
301
+ expect(Dummy.new.avatar).not_to be_a(Paperclip::Storage::Filesystem)
302
+ end
303
+ end
304
+
305
+ describe "An attachment with Azure storage and container defined as a Proc" do
306
+ before do
307
+ rebuild_model storage: :azure,
308
+ container: lambda { |attachment| "container_#{attachment.instance.other}" },
309
+ azure_credentials: {not: :important}
310
+ end
311
+
312
+ it "gets the right container name" do
313
+ expect(Dummy.new(other: 'a').avatar.container_name).to eq("container_a")
314
+ expect(Dummy.new(other: 'b').avatar.container_name).to eq("container_b")
315
+ end
316
+ end
317
+
318
+ context "An attachment with Azure storage and Azure credentials defined as a Proc" do
319
+ before do
320
+ rebuild_model storage: :azure,
321
+ container: {not: :important},
322
+ azure_credentials: lambda { |attachment|
323
+ Hash['storage_access_key' => "secret#{attachment.instance.other}"]
324
+ }
325
+ end
326
+
327
+ it "gets the right credentials" do
328
+ expect(Dummy.new(other: '1234').avatar.azure_credentials[:storage_access_key]).to eq("secret1234")
329
+ end
330
+ end
331
+
332
+ context "An attachment with Azure storage and Azure credentials in an unsupported manor" do
333
+ before do
334
+ rebuild_model storage: :azure,
335
+ container: "testing",
336
+ azure_credentials: ["unsupported"]
337
+ @dummy = Dummy.new
338
+ end
339
+
340
+ it "does not accept the credentials" do
341
+ expect { @dummy.avatar.azure_credentials }.to raise_error(ArgumentError)
342
+ end
343
+ end
344
+
345
+ context "An attachment with Azure storage and Azure credentials not supplied" do
346
+ before do
347
+ rebuild_model storage: :azure, container: "testing"
348
+ @dummy = Dummy.new
349
+ end
350
+
351
+ it "does not parse any credentials" do
352
+ expect(@dummy.avatar.azure_credentials).to eq({})
353
+ end
354
+ end
355
+
356
+ describe "with Azure credentials supplied as Pathname" do
357
+ before do
358
+ ENV['AZURE_CONTAINER'] = 'pathname_container'
359
+ ENV['AZURE_STORAGE_ACCOUNT'] = 'pathname_storage_account'
360
+ ENV['AZURE_STORAGE_ACCESS_KEY'] = storage_access_key
361
+
362
+ rails_env('test') do
363
+ rebuild_model storage: :azure,
364
+ azure_credentials: Pathname.new(fixture_file('azure.yml'))
365
+
366
+ Dummy.delete_all
367
+ @dummy = Dummy.new
368
+ end
369
+ end
370
+
371
+ it "parses the credentials" do
372
+ expect(@dummy.avatar.container_name).to eq('pathname_container')
373
+ end
374
+ end
375
+
376
+ describe "with Azure credentials in a YAML file" do
377
+ before do
378
+ ENV['AZURE_CONTAINER'] = 'pathname_container'
379
+ ENV['AZURE_STORAGE_ACCOUNT'] = 'pathname_storage_account'
380
+ ENV['AZURE_STORAGE_ACCESS_KEY'] = storage_access_key
381
+
382
+ rails_env('test') do
383
+ rebuild_model storage: :azure,
384
+ azure_credentials: File.new(fixture_file('azure.yml'))
385
+
386
+ Dummy.delete_all
387
+
388
+ @dummy = Dummy.new
389
+ end
390
+ end
391
+
392
+ it "runs the file through ERB" do
393
+ expect(@dummy.avatar.container_name).to eq('pathname_container')
394
+ end
395
+ end
396
+
397
+ describe "path is a proc" do
398
+ before do
399
+ rebuild_model storage: :azure,
400
+ path: ->(attachment) { attachment.instance.attachment_path }
401
+
402
+ @dummy = Dummy.new
403
+ @dummy.class_eval do
404
+ def attachment_path
405
+ '/some/dynamic/path'
406
+ end
407
+ end
408
+ @dummy.avatar = stringy_file
409
+ end
410
+
411
+ it "returns a correct path" do
412
+ expect(@dummy.avatar.path).to eq('/some/dynamic/path')
413
+ end
414
+ end
415
+
416
+ private
417
+
418
+ def rails_env(env)
419
+ stored_env, Rails.env = Rails.env, env
420
+ begin
421
+ yield
422
+ ensure
423
+ Rails.env = stored_env
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+ require 'active_record'
4
+ require 'active_record/version'
5
+ require 'active_support'
6
+ require 'active_support/core_ext'
7
+ require 'ostruct'
8
+ require 'pathname'
9
+ require 'activerecord-import'
10
+ require 'yaml'
11
+ require 'paperclip'
12
+
13
+ ROOT = Pathname(File.expand_path(File.join(File.dirname(__FILE__), '..')))
14
+
15
+ $LOAD_PATH << File.join(ROOT, 'lib')
16
+ $LOAD_PATH << File.join(ROOT, 'lib', 'paperclip')
17
+ require File.join(ROOT, 'lib', 'paperclip-azure.rb')
18
+
19
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
20
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
21
+ ActiveRecord::Base.establish_connection(config['test'])
22
+ if ActiveRecord::VERSION::STRING >= "4.2" &&
23
+ ActiveRecord::VERSION::STRING < "5.0"
24
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
25
+ end
26
+ Paperclip.options[:logger] = ActiveRecord::Base.logger
27
+
28
+ Dir[File.join(ROOT, 'spec', 'support', '**', '*.rb')].each{|f| require f }
29
+
30
+ Rails = FakeRails.new('test', Pathname.new(ROOT).join('tmp'))
31
+ ActiveSupport::Deprecation.silenced = true
32
+
33
+ RSpec.configure do |config|
34
+ config.include ModelReconstruction
35
+ config.include TestData
36
+ config.extend VersionHelper
37
+ config.before(:all) do
38
+ rebuild_model
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ class FakeModel
2
+ attr_accessor(
3
+ :avatar_file_name,
4
+ :avatar_file_size,
5
+ :avatar_updated_at,
6
+ :avatar_content_type,
7
+ :avatar_fingerprint,
8
+ :id
9
+ )
10
+
11
+ def errors
12
+ @errors ||= []
13
+ end
14
+
15
+ def run_paperclip_callbacks name, *args
16
+ end
17
+
18
+ def valid?
19
+ errors.empty?
20
+ end
21
+
22
+ def new_record?
23
+ false
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ class FakeRails
2
+ def initialize(env, root)
3
+ @env = env
4
+ @root = root
5
+ end
6
+
7
+ attr_accessor :env, :root
8
+
9
+ def const_defined?(const)
10
+ false
11
+ end
12
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ development:
2
+ storage_access_key: 54321
3
+ production:
4
+ storage_access_key: 12345
5
+ test:
6
+ container: <%= ENV['AZURE_CONTAINER'] %>
7
+ storage_account_name: <%= ENV['AZURE_STORAGE_ACCOUNT'] %>
8
+ storage_access_key: <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %>
@@ -0,0 +1 @@
1
+ This is not an image.
@@ -0,0 +1 @@
1
+ <html></html>
Binary file
@@ -0,0 +1,8 @@
1
+ development:
2
+ provider: AWS
3
+ aws_access_key_id: AWS_ID
4
+ aws_secret_access_key: AWS_SECRET
5
+ test:
6
+ provider: AWS
7
+ aws_access_key_id: AWS_ID
8
+ aws_secret_access_key: AWS_SECRET
Binary file
@@ -0,0 +1 @@
1
+ paperclip!
Binary file
Binary file
@@ -0,0 +1,24 @@
1
+ class MockAttachment
2
+ attr_accessor :updated_at, :original_filename
3
+ attr_reader :options
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ @model = options[:model]
8
+ @responds_to_updated_at = options[:responds_to_updated_at]
9
+ @updated_at = options[:updated_at]
10
+ @original_filename = options[:original_filename]
11
+ end
12
+
13
+ def instance
14
+ @model
15
+ end
16
+
17
+ def respond_to?(meth)
18
+ if meth.to_s == "updated_at"
19
+ @responds_to_updated_at || @updated_at
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ class MockInterpolator
2
+ def initialize(options = {})
3
+ @options = options
4
+ end
5
+
6
+ def interpolate(pattern, attachment, style_name)
7
+ @interpolated_pattern = pattern
8
+ @interpolated_attachment = attachment
9
+ @interpolated_style_name = style_name
10
+ @options[:result]
11
+ end
12
+
13
+ def has_interpolated_pattern?(pattern)
14
+ @interpolated_pattern == pattern
15
+ end
16
+
17
+ def has_interpolated_style_name?(style_name)
18
+ @interpolated_style_name == style_name
19
+ end
20
+
21
+ def has_interpolated_attachment?(attachment)
22
+ @interpolated_attachment == attachment
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ class MockUrlGeneratorBuilder
2
+ def initializer
3
+ end
4
+
5
+ def new(attachment)
6
+ @attachment = attachment
7
+ @attachment_options = @attachment.options
8
+ self
9
+ end
10
+
11
+ def for(style_name, options)
12
+ @generated_url_with_style_name = style_name
13
+ @generated_url_with_options = options
14
+ "hello"
15
+ end
16
+
17
+ def has_generated_url_with_options?(options)
18
+ # options.is_a_subhash_of(@generated_url_with_options)
19
+ options.inject(true) do |acc,(k,v)|
20
+ acc && @generated_url_with_options[k] == v
21
+ end
22
+ end
23
+
24
+ def has_generated_url_with_style_name?(style_name)
25
+ @generated_url_with_style_name == style_name
26
+ end
27
+ end