jd-paperclip-azure 3.0.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.
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