leifcr-refile 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/refile.js +125 -0
  3. data/config/locales/en.yml +10 -0
  4. data/config/routes.rb +5 -0
  5. data/lib/refile.rb +510 -0
  6. data/lib/refile/app.rb +186 -0
  7. data/lib/refile/attacher.rb +190 -0
  8. data/lib/refile/attachment.rb +108 -0
  9. data/lib/refile/attachment/active_record.rb +133 -0
  10. data/lib/refile/attachment_definition.rb +83 -0
  11. data/lib/refile/backend/file_system.rb +120 -0
  12. data/lib/refile/backend/s3.rb +1 -0
  13. data/lib/refile/backend_macros.rb +45 -0
  14. data/lib/refile/custom_logger.rb +48 -0
  15. data/lib/refile/file.rb +102 -0
  16. data/lib/refile/file_double.rb +13 -0
  17. data/lib/refile/image_processing.rb +1 -0
  18. data/lib/refile/rails.rb +54 -0
  19. data/lib/refile/rails/attachment_helper.rb +121 -0
  20. data/lib/refile/random_hasher.rb +11 -0
  21. data/lib/refile/signature.rb +36 -0
  22. data/lib/refile/simple_form.rb +17 -0
  23. data/lib/refile/type.rb +28 -0
  24. data/lib/refile/version.rb +3 -0
  25. data/spec/refile/active_record_helper.rb +35 -0
  26. data/spec/refile/app_spec.rb +424 -0
  27. data/spec/refile/attachment/active_record_spec.rb +568 -0
  28. data/spec/refile/attachment_helper_spec.rb +78 -0
  29. data/spec/refile/attachment_spec.rb +589 -0
  30. data/spec/refile/backend/file_system_spec.rb +5 -0
  31. data/spec/refile/backend_examples.rb +228 -0
  32. data/spec/refile/backend_macros_spec.rb +83 -0
  33. data/spec/refile/custom_logger_spec.rb +21 -0
  34. data/spec/refile/features/direct_upload_spec.rb +63 -0
  35. data/spec/refile/features/multiple_upload_spec.rb +122 -0
  36. data/spec/refile/features/normal_upload_spec.rb +144 -0
  37. data/spec/refile/features/presigned_upload_spec.rb +31 -0
  38. data/spec/refile/features/simple_form_spec.rb +8 -0
  39. data/spec/refile/fixtures/hello.txt +1 -0
  40. data/spec/refile/fixtures/image.jpg +0 -0
  41. data/spec/refile/fixtures/large.txt +44 -0
  42. data/spec/refile/fixtures/monkey.txt +1 -0
  43. data/spec/refile/fixtures/world.txt +1 -0
  44. data/spec/refile/spec_helper.rb +72 -0
  45. data/spec/refile_spec.rb +355 -0
  46. metadata +143 -0
@@ -0,0 +1,568 @@
1
+ require "refile/active_record_helper"
2
+ require "refile/attachment/active_record"
3
+
4
+ describe Refile::ActiveRecord::Attachment do
5
+ let(:options) { {} }
6
+ let(:required) { false }
7
+ let(:klass) do
8
+ opts = options
9
+ req = required
10
+ Class.new(ActiveRecord::Base) do
11
+ self.table_name = :posts
12
+
13
+ def self.name
14
+ "Post"
15
+ end
16
+
17
+ attachment :document, **opts
18
+ validates_presence_of :document, if: -> { req }
19
+ end
20
+ end
21
+
22
+ describe "#valid?" do
23
+ let(:options) { { type: :image, cache: :limited_cache } }
24
+
25
+ context "extension validation" do
26
+ let(:options) { { cache: :limited_cache, extension: %w[Png] } }
27
+
28
+ context "with file" do
29
+ let(:options) { { cache: :limited_cache, extension: %w[Png Gif] } }
30
+
31
+ it "returns true when extension is included in list" do
32
+ post = klass.new
33
+ post.document = Refile::FileDouble.new("hello", "image.Png")
34
+ expect(post.valid?).to be_truthy
35
+ expect(post.errors[:document]).to be_empty
36
+ end
37
+
38
+ it "returns true when extension is included in list but chars are randomcase" do
39
+ post = klass.new
40
+ post.document = Refile::FileDouble.new("hello", "image.PNG")
41
+ expect(post.valid?).to be_truthy
42
+ expect(post.errors[:document]).to be_empty
43
+ end
44
+
45
+ it "returns false and an error message when extension is invalid" do
46
+ post = klass.new
47
+ post.document = Refile::FileDouble.new("hello", "image.jpg")
48
+ expect(post.valid?).to be_falsy
49
+ expect(post.errors[:document]).to match_array([/not allowed to upload jpg.+Allowed types: Png[^,]?/])
50
+ end
51
+
52
+ it "returns false and an error message when extension is empty" do
53
+ post = klass.new
54
+ post.document = Refile::FileDouble.new("hello", "image")
55
+ expect(post.valid?).to be_falsy
56
+ expect(post.errors[:document]).to match_array([/not allowed to upload an empty.+Allowed types: Png[^,]?/])
57
+ end
58
+ end
59
+
60
+ context "with metadata" do
61
+ it "returns true when extension is included in list" do
62
+ file = Refile.cache.upload(StringIO.new("hello"))
63
+ post = klass.new
64
+ post.document = { id: file.id, filename: "image.Png", size: file.size }.to_json
65
+ expect(post.valid?).to be_truthy
66
+ expect(post.errors[:document]).to be_empty
67
+ end
68
+
69
+ it "returns true when extension is included in list but chars are randomcase" do
70
+ file = Refile.cache.upload(StringIO.new("hello"))
71
+ post = klass.new
72
+ post.document = { id: file.id, filename: "image.PNG", size: file.size }.to_json
73
+ expect(post.valid?).to be_truthy
74
+ expect(post.errors[:document]).to be_empty
75
+ end
76
+
77
+ it "returns false and an error message when extension is invalid" do
78
+ file = Refile.cache.upload(StringIO.new("hello"))
79
+ post = klass.new
80
+ post.document = { id: file.id, filename: "image.gif", size: file.size }.to_json
81
+ expect(post.valid?).to be_falsy
82
+ expect(post.errors[:document]).to match_array([/not allowed to upload gif.+Allowed types: Png[^,]?/])
83
+ end
84
+
85
+ it "returns false and an error message when extension is empty" do
86
+ file = Refile.cache.upload(StringIO.new("hello"))
87
+ post = klass.new
88
+ post.document = { id: file.id, filename: "image", size: file.size }.to_json
89
+ expect(post.valid?).to be_falsy
90
+ expect(post.errors[:document]).to match_array([/not allowed to upload an empty.+Allowed types: Png[^,]?/])
91
+ end
92
+
93
+ it "returns false when file size is zero" do
94
+ file = Refile.cache.upload(StringIO.new("hello"))
95
+ post = klass.new
96
+ post.document = { id: file.id, filename: "image.Png" }.to_json
97
+ expect(post.valid?).to be_falsy
98
+ expect(post.errors[:document].length).to eq(1)
99
+ end
100
+ end
101
+
102
+ context "extension as Proc" do
103
+ context "Proc returns an array with extensions" do
104
+ let(:options) { { cache: :limited_cache, extension: -> { ["gif"] } } }
105
+
106
+ it "returns true when extension is included in list" do
107
+ post = klass.new
108
+ post.document = Refile::FileDouble.new("hello", "funny.gif")
109
+ expect(post.valid?).to be_truthy
110
+ expect(post.errors[:document]).to be_empty
111
+ end
112
+ end
113
+
114
+ context "Proc returns nil" do
115
+ let(:options) { { cache: :limited_cache, extension: -> {} } }
116
+
117
+ it "returns true when extension is included in list" do
118
+ post = klass.new
119
+ post.document = Refile::FileDouble.new("hello", "funny.gif")
120
+ expect(post.valid?).to be_falsey
121
+ expect(post.errors[:document].length).to eq(1)
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ context "with file" do
128
+ it "returns true when no file is assigned" do
129
+ post = klass.new
130
+ expect(post.valid?).to be_truthy
131
+ expect(post.errors[:document]).to be_empty
132
+ end
133
+
134
+ it "returns false and an error message when type is invalid" do
135
+ post = klass.new
136
+ post.document = Refile::FileDouble.new("hello", content_type: "text/plain")
137
+ expect(post.valid?).to be_falsy
138
+ expect(post.errors[:document]).to match_array(
139
+ [%r{not allowed to upload text/plain.+Allowed types: image/jpeg, image/gif, and image/png[^,]?}]
140
+ )
141
+ end
142
+
143
+ it "returns false and an error message when type is empty" do
144
+ post = klass.new
145
+ post.document = Refile::FileDouble.new("hello", content_type: "")
146
+ expect(post.valid?).to be_falsy
147
+ expect(post.errors[:document]).to match_array(
148
+ [%r{not allowed to upload an empty.+Allowed types: image/jpeg, image/gif, and image/png[^,]?}]
149
+ )
150
+ end
151
+
152
+ it "returns false and error messages when it has multiple errors" do
153
+ post = klass.new
154
+ post.document = Refile::FileDouble.new("h" * 200, content_type: "text/plain")
155
+ expect(post.valid?).to be_falsy
156
+ expect(post.errors[:document]).to match_array(
157
+ [
158
+ %r{not allowed to upload text/plain.+Allowed types: image/jpeg, image/gif, and image/png[^,]?},
159
+ "is too large"
160
+ ]
161
+ )
162
+ end
163
+
164
+ it "returns true when type is valid" do
165
+ post = klass.new
166
+ post.document = Refile::FileDouble.new("hello", content_type: "image/png")
167
+ expect(post.valid?).to be_truthy
168
+ expect(post.errors[:document]).to be_empty
169
+ end
170
+
171
+ it "returns true when an encoding is appended to a valid type" do
172
+ post = klass.new
173
+ post.document = Refile::FileDouble.new("hello", content_type: "image/png;charset=UTF-8")
174
+ expect(post.valid?).to be_truthy
175
+ expect(post.errors[:document]).to be_empty
176
+ end
177
+ end
178
+
179
+ context "when file is required" do
180
+ let(:required) { true }
181
+ let(:options) { {} }
182
+
183
+ it "returns false without file" do
184
+ post = klass.new
185
+ expect(post.valid?).to be_falsy
186
+ end
187
+
188
+ it "returns true with file" do
189
+ post = klass.new
190
+ post.document = Refile::FileDouble.new("hello", "image.png")
191
+ expect(post.valid?).to be_truthy
192
+ end
193
+
194
+ it "returns false when nil is assigned after a file" do
195
+ post = klass.new
196
+ post.document = Refile::FileDouble.new("hello", "image.png")
197
+ post.document = nil
198
+ expect(post.valid?).to be_falsy
199
+ end
200
+ end
201
+
202
+ context "with metadata" do
203
+ it "returns false when metadata doesn't have an id" do
204
+ file = Refile.cache.upload(StringIO.new("hello"))
205
+ post = klass.new
206
+ post.document = { content_type: "text/png", size: file.size }.to_json
207
+ expect(post.valid?).to be_falsy
208
+ expect(post.errors[:document]).to match_array([%r{not allowed to upload text/png.+Allowed types: image/jpeg, image/gif, and image/png[^,]?}])
209
+ end
210
+
211
+ it "returns false and an error message when type is invalid" do
212
+ file = Refile.cache.upload(StringIO.new("hello"))
213
+ post = klass.new
214
+ post.document = { id: file.id, content_type: "text/png", size: file.size }.to_json
215
+ expect(post.valid?).to be_falsy
216
+ expect(post.errors[:document]).to match_array([%r{not allowed to upload text/png.+Allowed types: image/jpeg, image/gif, and image/png[^,]?}])
217
+ end
218
+
219
+ it "returns false and an error message when type is empty" do
220
+ file = Refile.cache.upload(StringIO.new("hello"))
221
+ post = klass.new
222
+ post.document = { id: file.id, content_type: "", size: file.size }.to_json
223
+ expect(post.valid?).to be_falsy
224
+ expect(post.errors[:document]).to match_array([%r{not allowed to upload an empty.+Allowed types: image/jpeg, image/gif, and image/png[^,]?}])
225
+ end
226
+
227
+ it "returns false when file size is zero" do
228
+ file = Refile.cache.upload(StringIO.new(""))
229
+ post = klass.new
230
+ post.document = { id: file.id, content_type: "image/png", size: file.size }.to_json
231
+ expect(post.valid?).to be_falsy
232
+ expect(post.errors[:document].length).to eq(1)
233
+ end
234
+
235
+ it "returns true when type is valid" do
236
+ file = Refile.cache.upload(StringIO.new("hello"))
237
+ post = klass.new
238
+ post.document = { id: file.id, content_type: "image/png", size: file.size }.to_json
239
+ expect(post.valid?).to be_truthy
240
+ expect(post.errors[:document]).to be_empty
241
+ end
242
+
243
+ it "returns true when an encoding is appended to a valid type" do
244
+ file = Refile.cache.upload(StringIO.new("hello"))
245
+ post = klass.new
246
+ post.document = { id: file.id, content_type: "image/png;charset=UTF-8", size: file.size }.to_json
247
+ expect(post.valid?).to be_truthy
248
+ expect(post.errors[:document]).to be_empty
249
+ end
250
+ end
251
+ end
252
+
253
+ describe "#save" do
254
+ it "stores the assigned file" do
255
+ post = klass.new
256
+ post.document = Refile::FileDouble.new("hello")
257
+ post.save
258
+ post = klass.find(post.id)
259
+ expect(post.document.read).to eq("hello")
260
+ expect(Refile.store.read(post.document.id)).to eq("hello")
261
+ end
262
+
263
+ it "replaces an existing file" do
264
+ post = klass.new
265
+ post.document = Refile::FileDouble.new("hello")
266
+ post.save
267
+ old_document = post.document
268
+
269
+ post = klass.find(post.id)
270
+ post.document = Refile::FileDouble.new("hello")
271
+ post.save
272
+
273
+ expect(Refile.store.read(post.document.id)).to eq("hello")
274
+ expect(post.document.id).not_to be eq old_document.id
275
+ end
276
+ end
277
+
278
+ describe "#destroy" do
279
+ context "default behaviour" do
280
+ it "removes the stored file" do
281
+ post = klass.new
282
+ post.document = Refile::FileDouble.new("hello")
283
+ post.save
284
+ file = post.document
285
+ post.destroy
286
+ expect(file.exists?).to be_falsy
287
+ end
288
+ end
289
+
290
+ context "with destroy: true" do
291
+ let(:options) { { destroy: true } }
292
+
293
+ it "removes the stored file" do
294
+ post = klass.new
295
+ post.document = Refile::FileDouble.new("hello")
296
+ post.save
297
+ file = post.document
298
+ post.destroy
299
+ expect(file.exists?).to be_falsy
300
+ end
301
+ end
302
+
303
+ context "with destroy: false" do
304
+ let(:options) { { destroy: false } }
305
+
306
+ it "does not remove the stored file" do
307
+ post = klass.new
308
+ post.document = Refile::FileDouble.new("hello")
309
+ post.save
310
+ file = post.document
311
+ post.destroy
312
+ expect(file.exists?).to be_truthy
313
+ end
314
+ end
315
+ end
316
+
317
+ describe ".accepts_nested_attributes_for" do
318
+ let(:options) { {} }
319
+ let(:post_class) do
320
+ opts = options
321
+ foo = document_class
322
+ Class.new(ActiveRecord::Base) do
323
+ self.table_name = :posts
324
+
325
+ def self.name
326
+ "Post"
327
+ end
328
+
329
+ has_many :documents, anonymous_class: foo, dependent: :destroy
330
+ accepts_attachments_for :documents, **opts
331
+ end
332
+ end
333
+
334
+ let(:document_class) do
335
+ Class.new(ActiveRecord::Base) do
336
+ self.table_name = :documents
337
+
338
+ def self.name
339
+ "Document"
340
+ end
341
+
342
+ attachment :file
343
+ end
344
+ end
345
+
346
+ let(:post) { post_class.new }
347
+
348
+ describe "#:association_:name" do
349
+ let(:wrong_method) { "files" }
350
+ let(:wrong_association_message) do
351
+ "wrong association name #{wrong_method}, use like this documents_files"
352
+ end
353
+
354
+ it "returns a friendly error message for wrong association name" do
355
+ expect { post.send(wrong_method) }.to raise_error(wrong_association_message)
356
+ end
357
+
358
+ it "return method missing" do
359
+ expect { post.foo }.to_not raise_error(wrong_association_message)
360
+ end
361
+
362
+ it "builds records from assigned files" do
363
+ post.documents_files = [Refile::FileDouble.new("hello"), Refile::FileDouble.new("world")]
364
+ expect(post.documents[0].file.read).to eq("hello")
365
+ expect(post.documents[1].file.read).to eq("world")
366
+ expect(post.documents.size).to eq(2)
367
+ end
368
+
369
+ it "builds records from cache" do
370
+ post.documents_files = [
371
+ [
372
+ { id: Refile.cache.upload(Refile::FileDouble.new("hello")).id },
373
+ { id: Refile.cache.upload(Refile::FileDouble.new("world")).id }
374
+ ].to_json
375
+ ]
376
+ expect(post.documents[0].file.read).to eq("hello")
377
+ expect(post.documents[1].file.read).to eq("world")
378
+ expect(post.documents.size).to eq(2)
379
+ end
380
+
381
+ it "prefers newly uploaded files over cache" do
382
+ post.documents_files = [
383
+ [
384
+ { id: Refile.cache.upload(Refile::FileDouble.new("moo")).id }
385
+ ].to_json,
386
+ Refile::FileDouble.new("hello"),
387
+ Refile::FileDouble.new("world")
388
+ ]
389
+ expect(post.documents[0].file.read).to eq("hello")
390
+ expect(post.documents[1].file.read).to eq("world")
391
+ expect(post.documents.size).to eq(2)
392
+ end
393
+
394
+ it "clears previously assigned files" do
395
+ post.documents_files = [
396
+ Refile::FileDouble.new("hello"),
397
+ Refile::FileDouble.new("world")
398
+ ]
399
+ post.save
400
+ post.update_attributes documents_files: [
401
+ Refile::FileDouble.new("foo")
402
+ ]
403
+ retrieved = post_class.find(post.id)
404
+ expect(retrieved.documents[0].file.read).to eq("foo")
405
+ expect(retrieved.documents.size).to eq(1)
406
+ end
407
+
408
+ context "with append: true" do
409
+ let(:options) { { append: true } }
410
+
411
+ it "appends to previously assigned files" do
412
+ post.documents_files = [
413
+ Refile::FileDouble.new("hello"),
414
+ Refile::FileDouble.new("world")
415
+ ]
416
+ post.save
417
+ post.update_attributes documents_files: [
418
+ Refile::FileDouble.new("foo")
419
+ ]
420
+ retrieved = post_class.find(post.id)
421
+ expect(retrieved.documents[0].file.read).to eq("hello")
422
+ expect(retrieved.documents[1].file.read).to eq("world")
423
+ expect(retrieved.documents[2].file.read).to eq("foo")
424
+ expect(retrieved.documents.size).to eq(3)
425
+ end
426
+
427
+ it "appends to previously assigned files with cached files" do
428
+ post.documents_files = [
429
+ Refile::FileDouble.new("hello"),
430
+ Refile::FileDouble.new("world")
431
+ ]
432
+ post.save
433
+ post.update_attributes documents_files: [
434
+ [{
435
+ id: Refile.cache.upload(Refile::FileDouble.new("hello")).id,
436
+ filename: "some.jpg",
437
+ content_type: "image/jpeg",
438
+ size: 1234
439
+ }].to_json
440
+ ]
441
+ retrieved = post_class.find(post.id)
442
+ expect(retrieved.documents.size).to eq(3)
443
+ end
444
+ end
445
+ end
446
+
447
+ describe "#:association_:name_data" do
448
+ it "returns metadata of all files" do
449
+ post.documents_files = [nil, Refile::FileDouble.new("hello"), Refile::FileDouble.new("world")]
450
+ data = post.documents_files_data
451
+ expect(Refile.cache.read(data[0][:id])).to eq("hello")
452
+ expect(Refile.cache.read(data[1][:id])).to eq("world")
453
+ expect(data.size).to eq(2)
454
+ end
455
+ end
456
+ end
457
+
458
+ context "when attachment assigned to nested model" do
459
+ let(:base_users_class) do
460
+ Class.new(ActiveRecord::Base) do
461
+ self.table_name = :users
462
+
463
+ def self.name
464
+ "User"
465
+ end
466
+ end
467
+ end
468
+
469
+ context "when model has one nested attachment" do
470
+ let(:users_class) do
471
+ posts_class = klass
472
+ base_users_class.tap do |klass|
473
+ klass.instance_eval do
474
+ has_one :post, anonymous_class: posts_class
475
+ accepts_nested_attributes_for :post
476
+ end
477
+ end
478
+ end
479
+
480
+ describe "#save" do
481
+ it "stores the assigned file" do
482
+ user = users_class.create! post_attributes: { document: Refile::FileDouble.new("foo") }
483
+
484
+ post = user.post.reload
485
+ expect(post.document.read).to eq("foo")
486
+ expect(Refile.store.read(post.document.id)).to eq("foo")
487
+ end
488
+
489
+ it "removes files marked for removal" do
490
+ user = users_class.create!
491
+ post = klass.create!(user_id: user.id, document: Refile::FileDouble.new("foo"))
492
+
493
+ user.update_attributes!(post_attributes: { id: post.id, remove_document: true })
494
+
495
+ expect(post.reload.document).to be_nil
496
+ end
497
+
498
+ it "replaces an existing file" do
499
+ user = users_class.create! post_attributes: { document: Refile::FileDouble.new("foo") }
500
+ post = user.post
501
+
502
+ user.update! post_attributes: { id: post.id, document: Refile::FileDouble.new("bar") }
503
+
504
+ post.reload
505
+ expect(post.document.read).to eq("bar")
506
+ expect(Refile.store.read(post.document.id)).to eq("bar")
507
+ end
508
+ end
509
+ end
510
+
511
+ context "when model has many nested attachments" do
512
+ let(:users_class) do
513
+ posts_class = klass
514
+ base_users_class.tap do |klass|
515
+ klass.instance_eval do
516
+ has_many :posts, anonymous_class: posts_class
517
+ accepts_nested_attributes_for :posts
518
+ end
519
+ end
520
+ end
521
+
522
+ describe "#save" do
523
+ it "stores the assigned file" do
524
+ user = users_class.create! posts_attributes: [{ document: Refile::FileDouble.new("foo") }]
525
+
526
+ post = user.posts.first.reload
527
+ expect(post.document.read).to eq("foo")
528
+ expect(Refile.store.read(post.document.id)).to eq("foo")
529
+ end
530
+
531
+ it "removes files marked for removal" do
532
+ user = users_class.create!
533
+ post = klass.create!(user_id: user.id, document: Refile::FileDouble.new("foo"))
534
+
535
+ user.update_attributes!(posts_attributes: { id: post.id, remove_document: true })
536
+
537
+ expect(post.reload.document).to be_nil
538
+ end
539
+
540
+ it "replaces an existing file" do
541
+ user = users_class.create! posts_attributes: [{ document: Refile::FileDouble.new("foo") }]
542
+ post = user.posts.first
543
+ user.update! posts_attributes: [{ id: post.id, document: Refile::FileDouble.new("bar") }]
544
+
545
+ post.reload
546
+ expect(post.document.read).to eq("bar")
547
+ expect(Refile.store.read(post.document.id)).to eq("bar")
548
+ end
549
+ end
550
+ end
551
+ end
552
+
553
+ context "when assigned to an attribute that does not track changes" do
554
+ let(:klass) do
555
+ Class.new(ActiveRecord::Base) do
556
+ self.table_name = :posts
557
+
558
+ attachment :not_trackable_attribute
559
+ end
560
+ end
561
+
562
+ it "assigns the file to the attribute" do
563
+ post = klass.new
564
+ post.not_trackable_attribute = Refile::FileDouble.new("foo")
565
+ expect(post.not_trackable_attribute.read).to eq("foo")
566
+ end
567
+ end
568
+ end