attachment_saver 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +3 -0
  2. data/MIT-LICENSE +19 -0
  3. data/README +137 -0
  4. data/Rakefile +16 -0
  5. data/attachment_saver.gemspec +41 -0
  6. data/init.rb +1 -0
  7. data/lib/attachment_saver.rb +171 -0
  8. data/lib/attachment_saver/version.rb +3 -0
  9. data/lib/attachment_saver_errors.rb +3 -0
  10. data/lib/datastores/file_system.rb +189 -0
  11. data/lib/datastores/in_column.rb +49 -0
  12. data/lib/misc/extended_tempfile.rb +12 -0
  13. data/lib/misc/file_size.rb +5 -0
  14. data/lib/misc/image_science_extensions.rb +102 -0
  15. data/lib/misc/mini_magick_extensions.rb +89 -0
  16. data/lib/processors/image.rb +187 -0
  17. data/lib/processors/image_science.rb +94 -0
  18. data/lib/processors/mini_magick.rb +103 -0
  19. data/lib/processors/r_magick.rb +120 -0
  20. data/test/attachment_saver_test.rb +162 -0
  21. data/test/database.yml +3 -0
  22. data/test/file_system_datastore_test.rb +468 -0
  23. data/test/fixtures/broken.jpg +1 -0
  24. data/test/fixtures/emptyextension. +0 -0
  25. data/test/fixtures/noextension +0 -0
  26. data/test/fixtures/test.jpg +0 -0
  27. data/test/fixtures/test.js +1 -0
  28. data/test/fixtures/wrongextension.png +0 -0
  29. data/test/image_fixtures.rb +69 -0
  30. data/test/image_operations.rb +114 -0
  31. data/test/image_processor_test.rb +67 -0
  32. data/test/image_processor_test_common.rb +81 -0
  33. data/test/image_science_processor_test.rb +20 -0
  34. data/test/in_column_datastore_test.rb +115 -0
  35. data/test/mini_magick_processor_test.rb +20 -0
  36. data/test/model_test.rb +205 -0
  37. data/test/public/.empty +0 -0
  38. data/test/rmagick_processor_test.rb +20 -0
  39. data/test/schema.rb +41 -0
  40. data/test/test_helper.rb +49 -0
  41. metadata +223 -0
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: attachment_saver_test_sqlite.db
@@ -0,0 +1,468 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+ require 'mocha'
3
+ require 'datastores/file_system'
4
+
5
+ class FileSystemDatastoreTest < Test::Unit::TestCase
6
+ attr_accessor :storage_key, :original_filename, :content_type
7
+
8
+ DEFAULT_ATTACHMENT_OPTIONS = {:storage_directory => File.join(TEST_TEMP_DIR, 'fs_store_test'),
9
+ :storage_path_base => 'files'}
10
+
11
+ def self.attachment_options
12
+ @@attachment_options ||= DEFAULT_ATTACHMENT_OPTIONS
13
+ end
14
+
15
+ include AttachmentSaver::DataStores::FileSystem
16
+
17
+ def setup
18
+ @test_filename = File.join(self.class.attachment_options[:storage_directory], self.class.attachment_options[:storage_path_base], "test#{$$}.dat")
19
+ @uploaded_data = nil
20
+ @uploaded_file = nil
21
+ @saved_to = nil
22
+ self.storage_key = nil
23
+ self.original_filename = 'myfile.dat'
24
+ self.content_type = 'application/octet-stream'
25
+ end
26
+
27
+ def teardown
28
+ FileUtils.rm_rf(TEST_TEMP_DIR)
29
+ end
30
+
31
+ def file_extension
32
+ "testext"
33
+ end
34
+
35
+ def random_data(length = nil)
36
+ length = 512 + rand(2048) if length.nil?
37
+ Array.new(length).collect {rand(256)} .pack('C*')
38
+ end
39
+
40
+ def read_file(filename)
41
+ File.read(filename, :encoding => "ascii-8bit") # ruby 1.9
42
+ rescue TypeError
43
+ File.read(filename) # ruby 1.8
44
+ end
45
+
46
+ def save_attachment_to_with_record(filename)
47
+ @saved_to = filename
48
+ save_attachment_to_without_record(filename)
49
+ end
50
+ alias_method_chain :save_attachment_to, :record
51
+
52
+
53
+ def save_attachment_to_test(expected_data)
54
+ File.rm(@test_filename) if File.exist?(@test_filename)
55
+
56
+ save_attachment_to(@test_filename)
57
+
58
+ assert File.exist?(@test_filename), "no file #{@test_filename} created"
59
+ assert expected_data == read_file(@test_filename), "data written to #{@test_filename} doesn't match"
60
+ end
61
+
62
+ def save_attachment_to_test_no_clobber_existing
63
+ FileUtils.mkdir_p(File.dirname(@test_filename))
64
+ File.open(@test_filename, 'wb') {|f| f.write('test file')}
65
+
66
+ assert_raises(Errno::EEXIST) { save_attachment_to(@test_filename) }
67
+ assert File.exist?(@test_filename), "file #{@test_filename} deleted"
68
+ assert 'test file' == read_file(@test_filename), "contents of #{@test_filename} clobbered"
69
+ end
70
+
71
+ def save_test_independent_files(uploaded_file, original_data)
72
+ uploaded_file.rewind
73
+ uploaded_file.write('new file data')
74
+ uploaded_file.truncate('new file data'.length)
75
+ uploaded_file.flush
76
+ assert original_data == read_file(@test_filename), "stored file appears to be a hardlink to the uploaded file"
77
+ end
78
+
79
+ def save_test_same_file(uploaded_file)
80
+ uploaded_file.rewind
81
+ uploaded_file.write('new file data')
82
+ uploaded_file.truncate('new file data'.length)
83
+ uploaded_file.flush
84
+ assert 'new file data' == read_file(@test_filename), "stored file is not a hardlink to the uploaded file"
85
+ end
86
+
87
+ def test_save_attachment_to_for_data
88
+ @uploaded_data = data = random_data
89
+ @save_upload = true
90
+ save_attachment_to_test(data)
91
+ end
92
+
93
+ def test_save_attachment_to_for_data_doesnt_clobber_existing
94
+ @uploaded_data = random_data
95
+ @save_upload = true
96
+ save_attachment_to_test_no_clobber_existing
97
+ end
98
+
99
+ def test_save_attachment_to_for_file
100
+ data = random_data
101
+ FileUtils.mkdir_p(File.dirname(@test_filename))
102
+ File.open(@test_filename + '.src_file', 'wb+') do |file|
103
+ file.write(data)
104
+ @uploaded_file = file
105
+ @save_upload = true
106
+ save_attachment_to_test(data)
107
+ save_test_independent_files(file, data)
108
+ end
109
+ end
110
+
111
+ def test_save_attachment_to_for_file_doesnt_clobber_existing
112
+ FileUtils.mkdir_p(File.dirname(@test_filename))
113
+ File.open(@test_filename + '.src_file', 'wb') do |file|
114
+ file.write(random_data)
115
+ @uploaded_file = file
116
+ @save_upload = true
117
+ save_attachment_to_test_no_clobber_existing
118
+ end
119
+ end
120
+
121
+ def test_save_attachment_to_for_tempfile
122
+ data = random_data
123
+ FileUtils.mkdir_p(File.dirname(@test_filename))
124
+ Tempfile.open('src_file', File.dirname(@test_filename)) do |file|
125
+ file.write(data)
126
+ @uploaded_file = file
127
+ @save_upload = true
128
+ save_attachment_to_test(data)
129
+ save_test_same_file(file)
130
+ end
131
+ end
132
+
133
+ def test_save_attachment_to_for_tempfile_doesnt_clobber_existing
134
+ FileUtils.mkdir_p(File.dirname(@test_filename))
135
+ Tempfile.open('src_file', File.dirname(@test_filename)) do |file|
136
+ file.write(random_data)
137
+ @uploaded_file = file
138
+ @save_upload = true
139
+ save_attachment_to_test_no_clobber_existing
140
+ end
141
+ end
142
+
143
+ def test_save_attachment_to_for_tempfile_falls_back_if_ln_fails
144
+ data = random_data
145
+ FileUtils.mkdir_p(File.dirname(@test_filename))
146
+ Tempfile.open('src_file', File.dirname(@test_filename)) do |file|
147
+ file.write(data)
148
+ @uploaded_file = file
149
+ @save_upload = true
150
+ FileUtils.expects(:ln).once.raises(RuntimeError)
151
+ save_attachment_to_test(data)
152
+ save_test_independent_files(file, data)
153
+ end
154
+ end
155
+
156
+
157
+ def test_save_attachment_without_upload
158
+ expects(:save_attachment_to).times(0)
159
+ expects(:process_attachment?).times(0)
160
+ expects(:process_attachment).times(0)
161
+ save_attachment
162
+ end
163
+
164
+
165
+ def test_save_attachment_with_random_filename
166
+ @uploaded_data = data = random_data
167
+ @save_upload = true
168
+ expects(:process_attachment?).times(1).returns(false)
169
+ expects(:process_attachment).times(0)
170
+
171
+ save_attachment
172
+
173
+ assert !storage_key.blank?, "storage_key not set"
174
+ assert File.exist?(storage_filename), "no file #{storage_filename} created"
175
+ assert data == read_file(storage_filename), "data written to #{storage_filename} doesn't match"
176
+ end
177
+
178
+ def test_save_attachment_with_random_filename_retries_for_a_while
179
+ @uploaded_data = random_data
180
+ @save_upload = true
181
+ expects(:save_attachment_to).times(100).raises(Errno::EEXIST)
182
+ expects(:process_attachment?).times(0)
183
+ assert_raises(FileSystemAttachmentDataStoreError) { save_attachment } # as above
184
+ end
185
+
186
+
187
+ class Named
188
+ attr_accessor :storage_key, :original_filename, :content_type
189
+
190
+ def self.attachment_options
191
+ @@attachment_options ||= DEFAULT_ATTACHMENT_OPTIONS.dup
192
+ @@attachment_options[:filter_filenames] ||= /[^\w\._-]/
193
+ @@attachment_options
194
+ end
195
+
196
+ def uploaded_data=(uploaded_data)
197
+ @uploaded_data = uploaded_data
198
+ @save_upload = true
199
+ end
200
+
201
+ include AttachmentSaver::InstanceMethods
202
+ include AttachmentSaver::DataStores::FileSystem
203
+ end
204
+
205
+ def test_save_new_attachment_with_filtered_filename
206
+ named = Named.new
207
+ named.uploaded_data = data = random_data
208
+ named.original_filename = 'm!y_file-test12+*.dat'
209
+ named.content_type = 'application/octet-stream'
210
+ named.expects(:process_attachment?).times(1).returns(false)
211
+ named.expects(:process_attachment).times(0)
212
+ named.save_attachment
213
+
214
+ assert !named.storage_key.blank?, "named storage_key not set"
215
+ assert_equal 'm_y_file-test12__.dat', named.storage_key.gsub(/.*\//, ''), "named storage key doesn't correspond to original filename"
216
+ assert File.exist?(named.storage_filename), "no named file #{named.storage_filename} created"
217
+ assert data == read_file(named.storage_filename), "data written to #{named.storage_filename} doesn't match"
218
+ end
219
+
220
+ def test_save_new_attachment_with_filtered_filename_retries_only_for_a_while
221
+ named = Named.new
222
+ named.uploaded_data = data = random_data
223
+ named.original_filename = 'm!y_file-test12+*.dat'
224
+ named.content_type = 'application/octet-stream'
225
+ named.expects(:save_attachment_to).times(100).raises(Errno::EEXIST)
226
+ assert_raises(FileSystemAttachmentDataStoreError) { named.save_attachment } # as above
227
+ end
228
+
229
+
230
+ class Thumbnail
231
+ attr_accessor :storage_key, :content_type, :original, :format_name
232
+
233
+ def self.attachment_options
234
+ @@attachment_options ||= DEFAULT_ATTACHMENT_OPTIONS.dup
235
+ end
236
+
237
+ def uploaded_data=(uploaded_data)
238
+ @uploaded_data = uploaded_data
239
+ @save_upload = true
240
+ end
241
+
242
+ include AttachmentSaver::InstanceMethods
243
+ include AttachmentSaver::DataStores::FileSystem
244
+ end
245
+
246
+ def test_save_new_attachment_with_parent_filename
247
+ @uploaded_data = random_data
248
+ @save_upload = true
249
+ expects(:process_attachment?).times(1).returns(false)
250
+ expects(:process_attachment).times(0)
251
+ save_attachment
252
+
253
+ thumbnail = Thumbnail.new
254
+ thumbnail.uploaded_data = thumbnail_data = random_data
255
+ thumbnail.content_type = 'application/octet-stream'
256
+ thumbnail.file_extension = 'test'
257
+ thumbnail.original = self
258
+ thumbnail.format_name = 'thumb'
259
+ thumbnail.expects(:process_attachment?).times(1).returns(false)
260
+ thumbnail.expects(:process_attachment).times(0)
261
+ thumbnail.save_attachment
262
+
263
+ assert !thumbnail.storage_key.blank?, "thumbnail storage_key not set"
264
+ assert_equal File.dirname(storage_key), File.dirname(thumbnail.storage_key), "thumbnail not saved to same directory as parent"
265
+ assert_equal storage_key.gsub(/\.\w+$/, '_thumb.test'), thumbnail.storage_key, "thumbnail storage key doesn't correspond to parent"
266
+ assert File.exist?(thumbnail.storage_filename), "no thumbnail file #{thumbnail.storage_filename} created"
267
+ assert thumbnail_data == read_file(thumbnail.storage_filename), "data written to #{thumbnail.storage_filename} doesn't match"
268
+ end
269
+
270
+ def test_save_new_attachment_with_filtered_parent_filename
271
+ named = Named.new
272
+ named.uploaded_data = data = random_data
273
+ named.original_filename = 'm!y_file-test12+*.dat'
274
+ named.content_type = 'application/octet-stream'
275
+ named.expects(:process_attachment?).times(1).returns(false)
276
+ named.expects(:process_attachment).times(0)
277
+ named.save_attachment
278
+
279
+ thumbnail = Thumbnail.new
280
+ thumbnail.uploaded_data = thumbnail_data = random_data
281
+ thumbnail.content_type = 'application/octet-stream'
282
+ thumbnail.file_extension = 'test'
283
+ thumbnail.original = named
284
+ thumbnail.format_name = 'thumb'
285
+ thumbnail.expects(:process_attachment?).times(1).returns(false)
286
+ thumbnail.expects(:process_attachment).times(0)
287
+ thumbnail.save_attachment
288
+
289
+ assert !thumbnail.storage_key.blank?, "thumbnail storage_key not set"
290
+ assert_equal File.dirname(named.storage_key), File.dirname(thumbnail.storage_key), "thumbnail not saved to same directory as parent"
291
+ assert_equal named.storage_key.gsub(/\.\w+$/, '_thumb.test'), thumbnail.storage_key, "thumbnail storage key doesn't correspond to parent"
292
+ assert File.exist?(thumbnail.storage_filename), "no thumbnail file #{thumbnail.storage_filename} created"
293
+ assert thumbnail_data == read_file(thumbnail.storage_filename), "data written to #{thumbnail.storage_filename} doesn't match"
294
+ end
295
+
296
+ def test_save_new_attachment_with_filtered_parent_filename_adds_suffix_if_existing
297
+ named = Named.new
298
+ named.uploaded_data = data = random_data
299
+ named.original_filename = 'm!y_file-test12+*.dat'
300
+ named.content_type = 'application/octet-stream'
301
+ named.expects(:process_attachment?).times(1).returns(false)
302
+ named.expects(:process_attachment).times(0)
303
+ named.save_attachment
304
+
305
+ FileUtils.touch(named.storage_filename.gsub(/\.\w+$/, '_thumb.test'))
306
+ FileUtils.touch(named.storage_filename.gsub(/\.\w+$/, '_thumb2.test'))
307
+
308
+ thumbnail = Thumbnail.new
309
+ thumbnail.uploaded_data = thumbnail_data = random_data
310
+ thumbnail.content_type = 'application/octet-stream'
311
+ thumbnail.file_extension = 'test'
312
+ thumbnail.original = named
313
+ thumbnail.format_name = 'thumb'
314
+ thumbnail.expects(:process_attachment?).times(1).returns(false)
315
+ thumbnail.expects(:process_attachment).times(0)
316
+ thumbnail.save_attachment
317
+
318
+ assert !thumbnail.storage_key.blank?, "thumbnail storage_key not set"
319
+ assert_equal File.dirname(named.storage_key), File.dirname(thumbnail.storage_key), "thumbnail not saved to same directory as parent"
320
+ assert_equal named.storage_key.gsub(/\.\w+$/, '_thumb3.test'), thumbnail.storage_key, "thumbnail storage key doesn't correspond to parent"
321
+ assert File.exist?(thumbnail.storage_filename), "no thumbnail file #{thumbnail.storage_filename} created"
322
+ assert thumbnail_data == read_file(thumbnail.storage_filename), "data written to #{thumbnail.storage_filename} doesn't match"
323
+ end
324
+
325
+
326
+ def test_save_attachment_calls_processing
327
+ @uploaded_data = expected_data = random_data
328
+ @save_upload = true
329
+ expects(:process_attachment?).times(1).returns(true)
330
+ expects(:process_attachment_with_wrapping).times(1).returns do |filename|
331
+ assert expected_data == read_file(filename)
332
+ end
333
+ expects(:save_attachment_to)
334
+
335
+ save_attachment
336
+ end
337
+
338
+
339
+ def test_save_attachment_deletes_immediately_if_processing_fails
340
+ @uploaded_data = random_data
341
+ @save_upload = true
342
+ expects(:process_attachment?).times(1).returns(true)
343
+ expects(:process_attachment_with_wrapping).times(1).raises(AttachmentProcessorError)
344
+
345
+ assert_raises(AttachmentProcessorError) { save_attachment }
346
+
347
+ assert_equal nil, storage_key, "storage key wasn't reset after processing failed"
348
+ assert_not_equal nil, @saved_to, "save_attachment_to not called"
349
+ assert !File.exist?(@saved_to), "saved file wasn't removed after processing failed"
350
+ end
351
+
352
+
353
+ def test_save_attachment_with_old_filename
354
+ expects(:process_attachment?).times(2).returns(false)
355
+
356
+ @uploaded_data = random_data
357
+ @save_upload = true
358
+ save_attachment
359
+ tidy_attachment # after_save
360
+ old_filename = storage_filename
361
+
362
+ @uploaded_data = random_data
363
+ @save_upload = true
364
+ save_attachment
365
+ tidy_attachment # after_save
366
+
367
+ assert_not_equal storage_filename, old_filename
368
+ assert !File.exist?(old_filename), "old file wasn't removed after save"
369
+ end
370
+
371
+ def test_save_attachment_with_old_filename_keeps_old_if_processing_fails
372
+ @uploaded_data = data = random_data
373
+ @save_upload = true
374
+ expects(:process_attachment?).times(1).returns(false)
375
+ save_attachment
376
+ tidy_attachment # after_save
377
+ old_key = storage_key
378
+ old_filename = storage_filename
379
+
380
+ @uploaded_data = random_data
381
+ @save_upload = true
382
+ expects(:process_attachment?).times(1).returns(true)
383
+ expects(:process_attachment_with_wrapping).times(1).raises(AttachmentProcessorError)
384
+ assert_raises(AttachmentProcessorError) { save_attachment }
385
+
386
+ assert_equal old_key, storage_key
387
+ assert File.exist?(old_filename), "old file was removed before save"
388
+ assert data == read_file(storage_filename), "data in old file damaged"
389
+
390
+ tidy_attachment # check the old-file deletion code wouldn't destroy it either (presumably after another save attempt)
391
+ assert File.exist?(old_filename), "old file was removed before save"
392
+ end
393
+
394
+
395
+ def test_destroy_attachment
396
+ @uploaded_data = random_data
397
+ @save_upload = true
398
+ expects(:process_attachment?).times(1).returns(false)
399
+ save_attachment
400
+ tidy_attachment # after_save
401
+
402
+ delete_attachment
403
+ assert !storage_key.blank?, "storage_key not set"
404
+ assert !File.exist?(storage_filename), "file #{storage_filename} not removed by destroy"
405
+ end
406
+
407
+
408
+ def test_public_path
409
+ self.storage_key = 'files/myfile.dat'
410
+ assert_equal '/files/myfile.dat', public_path
411
+ end
412
+
413
+
414
+ def test_in_storage?
415
+ @uploaded_data = random_data
416
+ @save_upload = true
417
+ expects(:process_attachment?).times(1).returns(false)
418
+ save_attachment
419
+
420
+ assert_equal true, in_storage?
421
+ FileUtils.rm(self.storage_filename)
422
+ assert_equal false, in_storage?
423
+ end
424
+
425
+
426
+ EXPECTED_DEFAULT_MODE = 0664
427
+ TEST_FILE_MODE = 0604
428
+
429
+ def test_default_permission_setting
430
+ assert_equal EXPECTED_DEFAULT_MODE, Named.attachment_options[:file_permissions] # using Named to check is arbitrary - just can't use this class, since we mess with in it the test below (we don't ever set it back, since none of the other tests care exactly what the permissions setting is)
431
+ end
432
+
433
+ def test_permission_setting_for_save_from_data
434
+ self.class.attachment_options[:file_permissions] = TEST_FILE_MODE
435
+ @uploaded_data = data = random_data
436
+ @save_upload = true
437
+ save_attachment_to_test(data)
438
+ assert_equal TEST_FILE_MODE, File.stat(@test_filename).mode & 0777
439
+ end
440
+
441
+ def test_permission_setting_for_save_from_file
442
+ self.class.attachment_options[:file_permissions] = TEST_FILE_MODE
443
+ data = random_data
444
+ FileUtils.mkdir_p(File.dirname(@test_filename))
445
+ File.open(@test_filename + '.src_file', 'wb+') do |file|
446
+ file.write(data)
447
+ @uploaded_file = file
448
+ @save_upload = true
449
+ save_attachment_to_test(data)
450
+ save_test_independent_files(file, data)
451
+ end
452
+ assert_equal TEST_FILE_MODE, File.stat(@test_filename).mode & 0777
453
+ end
454
+
455
+ def test_permission_setting_for_save_from_tempfile
456
+ self.class.attachment_options[:file_permissions] = TEST_FILE_MODE
457
+ data = random_data
458
+ FileUtils.mkdir_p(File.dirname(@test_filename))
459
+ Tempfile.open('src_file', File.dirname(@test_filename)) do |file|
460
+ file.write(data)
461
+ @uploaded_file = file
462
+ @save_upload = true
463
+ save_attachment_to_test(data)
464
+ save_test_same_file(file)
465
+ end
466
+ assert_equal TEST_FILE_MODE, File.stat(@test_filename).mode & 0777
467
+ end
468
+ end