leifcr-refile 0.6.3

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 (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