require 'spec_helper' require 'open-uri' describe 'Paperclip' do around do |example| files_before = ObjectSpace.each_object(Tempfile).select do |file| file.path && File.file?(file.path) end example.run files_after = ObjectSpace.each_object(Tempfile).select do |file| file.path && File.file?(file.path) end diff = files_after - files_before expect(diff).to eq([]), "Leaked tempfiles: #{diff.inspect}" end context "Many models at once" do before do rebuild_model @file = File.new(fixture_file("5k.png"), 'rb') # Deals with `Too many open files` error dummies = Array.new(300) { Dummy.new avatar: @file } Dummy.import dummies # save attachment instances to run after hooks including tempfile cleanup # since activerecord-import does not use our usually hooked-in hooks # (such as after_save) dummies.each { |dummy| dummy.avatar.save } end after { @file.close } it "does not exceed the open file limit" do assert_nothing_raised do Dummy.all.each { |dummy| dummy.avatar } end end end context "An attachment" do before do rebuild_model styles: { thumb: "50x50#" } @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') @dummy.avatar = @file assert @dummy.save end after { @file.close } it "creates its thumbnails properly" do assert_match(/\b50x50\b/, `identify "#{@dummy.avatar.path(:thumb)}"`) end context 'reprocessing with unreadable original' do before { File.chmod(0000, @dummy.avatar.path) } it "does not raise an error" do assert_nothing_raised do silence_stream(STDERR) do @dummy.avatar.reprocess! end end end it "returns false" do silence_stream(STDERR) do assert !@dummy.avatar.reprocess! end end after { File.chmod(0644, @dummy.avatar.path) } end context "redefining its attachment styles" do before do Dummy.class_eval do has_attached_file :avatar, styles: { thumb: "150x25#", dynamic: lambda { |a| '50x50#' } } end @d2 = Dummy.find(@dummy.id) @original_timestamp = @d2.avatar_updated_at @d2.avatar.reprocess! @d2.save end it "creates its thumbnails properly" do assert_match(/\b150x25\b/, `identify "#{@dummy.avatar.path(:thumb)}"`) assert_match(/\b50x50\b/, `identify "#{@dummy.avatar.path(:dynamic)}"`) end it "changes the timestamp" do assert_not_equal @original_timestamp, @d2.avatar_updated_at end end end context "Attachment" do before do @thumb_path = "tmp/public/system/dummies/avatars/000/000/001/thumb/5k.png" File.delete(@thumb_path) if File.exist?(@thumb_path) rebuild_model styles: { thumb: "50x50#" } @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') end after { @file.close } it "does not create the thumbnails upon saving when post-processing is disabled" do @dummy.avatar.post_processing = false @dummy.avatar = @file assert @dummy.save assert_file_not_exists @thumb_path end it "creates the thumbnails upon saving when post_processing is enabled" do @dummy.avatar.post_processing = true @dummy.avatar = @file assert @dummy.save assert_file_exists @thumb_path end end context "Attachment with no generated thumbnails" do before do @thumb_small_path = "tmp/public/system/dummies/avatars/000/000/001/thumb_small/5k.png" @thumb_large_path = "tmp/public/system/dummies/avatars/000/000/001/thumb_large/5k.png" File.delete(@thumb_small_path) if File.exist?(@thumb_small_path) File.delete(@thumb_large_path) if File.exist?(@thumb_large_path) rebuild_model styles: { thumb_small: "50x50#", thumb_large: "60x60#" } @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') @dummy.avatar.post_processing = false @dummy.avatar = @file assert @dummy.save @dummy.avatar.post_processing = true end after { @file.close } it "allows us to create all thumbnails in one go" do assert_file_not_exists(@thumb_small_path) assert_file_not_exists(@thumb_large_path) @dummy.avatar.reprocess! assert_file_exists(@thumb_small_path) assert_file_exists(@thumb_large_path) end it "allows us to selectively create each thumbnail" do skip <<-EXPLANATION #reprocess! calls #assign which calls Paperclip.io_adapters.for which creates the tempfile. #assign then calls #post_process_file which calls MediaTypeSpoofDetectionValidator#validate_each which calls Paperclip.io_adapters.for, which creates another tempfile. That first tempfile is the one that leaks. EXPLANATION assert_file_not_exists(@thumb_small_path) assert_file_not_exists(@thumb_large_path) @dummy.avatar.reprocess! :thumb_small assert_file_exists(@thumb_small_path) assert_file_not_exists(@thumb_large_path) @dummy.avatar.reprocess! :thumb_large assert_file_exists(@thumb_large_path) end end context "A model that modifies its original" do before do rebuild_model styles: { original: "2x2#" } @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') @dummy.avatar = @file end it "reports the file size of the processed file and not the original" do assert_not_equal File.size(@file.path), @dummy.avatar.size end after do @file.close # save attachment instance to run after hooks (including tempfile cleanup) @dummy.avatar.save end end context "A model with attachments scoped under an id" do before do rebuild_model styles: { large: "100x100", medium: "50x50" }, path: ":rails_root/tmp/:id/:attachments/:style.:extension" @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') @dummy.avatar = @file end after { @file.close } context "when saved" do before do @dummy.save @saved_path = @dummy.avatar.path(:large) end it "has a large file in the right place" do assert_file_exists(@dummy.avatar.path(:large)) end context "and deleted" do before do @dummy.avatar.clear @dummy.save end it "does not have a large file in the right place anymore" do assert_file_not_exists(@saved_path) end it "does not have its next two parent directories" do assert_file_not_exists(File.dirname(@saved_path)) assert_file_not_exists(File.dirname(File.dirname(@saved_path))) end end context 'and deleted where the delete fails' do it "does not die if an unexpected SystemCallError happens" do FileUtils.stubs(:rmdir).raises(Errno::EPIPE) assert_nothing_raised do @dummy.avatar.clear @dummy.save end end end end end [000,002,022].each do |umask| context "when the umask is #{umask}" do before do rebuild_model @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') @umask = File.umask(umask) end after do File.umask @umask @file.close end it "respects the current umask" do @dummy.avatar = @file @dummy.save assert_equal 0666&~umask, 0666&File.stat(@dummy.avatar.path).mode end end end [0666,0664,0640].each do |perms| context "when the perms are #{perms}" do before do rebuild_model override_file_permissions: perms @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') end after do @file.close end it "respects the current perms" do @dummy.avatar = @file @dummy.save assert_equal perms, File.stat(@dummy.avatar.path).mode & 0777 end end end it "skips chmod operation, when override_file_permissions is set to false (e.g. useful when using CIFS mounts)" do FileUtils.expects(:chmod).never rebuild_model override_file_permissions: false dummy = Dummy.create! dummy.avatar = @file dummy.save end context "A model with a filesystem attachment" do before do rebuild_model styles: { large: "300x300>", medium: "100x100", thumb: ["32x32#", :gif] }, default_style: :medium, url: "/:attachment/:class/:style/:id/:basename.:extension", path: ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension" @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), 'rb') @bad_file = File.new(fixture_file("bad.png"), 'rb') assert @dummy.avatar = @file assert @dummy.valid?, @dummy.errors.full_messages.join(", ") assert @dummy.save end after { [@file, @bad_file].each(&:close) } it "writes and delete its files" do [["434x66", :original], ["300x46", :large], ["100x15", :medium], ["32x32", :thumb]].each do |geo, style| cmd = %Q[identify -format "%wx%h" "#{@dummy.avatar.path(style)}"] assert_equal geo, `#{cmd}`.chomp, cmd end saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.path(s) } @d2 = Dummy.find(@dummy.id) assert_equal "100x15", `identify -format "%wx%h" "#{@d2.avatar.path}"`.chomp assert_equal "434x66", `identify -format "%wx%h" "#{@d2.avatar.path(:original)}"`.chomp assert_equal "300x46", `identify -format "%wx%h" "#{@d2.avatar.path(:large)}"`.chomp assert_equal "100x15", `identify -format "%wx%h" "#{@d2.avatar.path(:medium)}"`.chomp assert_equal "32x32", `identify -format "%wx%h" "#{@d2.avatar.path(:thumb)}"`.chomp assert @dummy.valid? assert @dummy.save saved_paths.each do |p| assert_file_exists(p) end @dummy.avatar.clear assert_nil @dummy.avatar_file_name assert @dummy.valid? assert @dummy.save saved_paths.each do |p| assert_file_not_exists(p) end @d2 = Dummy.find(@dummy.id) assert_nil @d2.avatar_file_name end it "works exactly the same when new as when reloaded" do @d2 = Dummy.find(@dummy.id) assert_equal @dummy.avatar_file_name, @d2.avatar_file_name [:thumb, :medium, :large, :original].each do |style| assert_equal @dummy.avatar.path(style), @d2.avatar.path(style) end saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.path(s) } @d2.avatar.clear assert @d2.save saved_paths.each do |p| assert_file_not_exists(p) end end it "does not abide things that don't have adapters" do assert_raises(Paperclip::AdapterRegistry::NoHandlerError) do @dummy.avatar = "not a file" end end it "is not ok with bad files" do @dummy.avatar = @bad_file assert ! @dummy.valid? # save attachment instance to run after hooks (including tempfile cleanup) @dummy.avatar.save end it "knows the difference between good files, bad files, and not files when validating" do Dummy.validates_attachment_presence :avatar @d2 = Dummy.find(@dummy.id) @d2.avatar = @file assert @d2.valid?, @d2.errors.full_messages.inspect # save attachment instance to run after hooks (including tempfile cleanup) @d2.avatar.save @d2.avatar = @bad_file assert ! @d2.valid? # save attachment instance to run after hooks (including tempfile cleanup) @d2.avatar.save end it "is able to reload without saving and not have the file disappear" do @dummy.avatar = @file assert @dummy.save, @dummy.errors.full_messages.inspect @dummy.avatar.clear assert_nil @dummy.avatar_file_name @dummy.reload assert_equal "5k.png", @dummy.avatar_file_name end context "that is assigned its file from another Paperclip attachment" do before do @dummy2 = Dummy.new @file2 = File.new(fixture_file("12k.png"), 'rb') assert @dummy2.avatar = @file2 @dummy2.save end after { @file2.close } it "works when assigned a file" do assert_not_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`, `identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"` assert @dummy.avatar = @dummy2.avatar @dummy.save assert_equal @dummy.avatar_file_name, @dummy2.avatar_file_name assert_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`, `identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"` end end end context "A model with an attachments association and a Paperclip attachment" do before do Dummy.class_eval do has_many :attachments, class_name: 'Dummy' end @file = File.new(fixture_file("5k.png"), 'rb') @dummy = Dummy.new @dummy.avatar = @file end after { @file.close } it "does not error when saving" do @dummy.save! end end context "A model with an attachment with hash in file name" do before do @settings = { styles: { thumb: "50x50#" }, path: ":rails_root/public/system/:attachment/:id_partition/:style/:hash.:extension", url: "/system/:attachment/:id_partition/:style/:hash.:extension", hash_secret: "somesecret" } rebuild_model @settings @file = File.new(fixture_file("5k.png"), 'rb') @dummy = Dummy.create! avatar: @file end after do @file.close end it "is accessible" do assert_file_exists(@dummy.avatar.path(:original)) assert_file_exists(@dummy.avatar.path(:thumb)) end context "when new style is added" do before do @dummy.avatar.options[:styles][:mini] = "25x25#" @dummy.avatar.instance_variable_set :@normalized_styles, nil Time.stubs(now: Time.now + 10) @dummy.avatar.reprocess! @dummy.reload end it "makes all the styles accessible" do assert_file_exists(@dummy.avatar.path(:original)) assert_file_exists(@dummy.avatar.path(:thumb)) assert_file_exists(@dummy.avatar.path(:mini)) end end end if ENV['S3_BUCKET'] def s3_files_for attachment [:thumb, :medium, :large, :original].inject({}) do |files, style| data = `curl "#{attachment.url(style)}" 2>/dev/null`.chomp t = Tempfile.new("paperclip-test") t.binmode t.write(data) t.rewind files[style] = t files end end def s3_headers_for attachment, style `curl --head "#{attachment.url(style)}" 2>/dev/null`.split("\n").inject({}) do |h,head| split_head = head.chomp.split(/\s*:\s*/, 2) h[split_head.first.downcase] = split_head.last unless split_head.empty? h end end context "A model with an S3 attachment" do before do rebuild_model( styles: { large: "300x300>", medium: "100x100", thumb: ["32x32#", :gif], custom: { geometry: "32x32#", s3_headers: { 'Cache-Control' => 'max-age=31557600' }, s3_metadata: { 'foo' => 'bar'} } }, storage: :s3, s3_credentials: File.new(fixture_file('s3.yml')), s3_options: { logger: Paperclip.logger }, default_style: :medium, bucket: ENV['S3_BUCKET'], path: ":class/:attachment/:id/:style/:basename.:extension" ) @dummy = Dummy.new @file = File.new(fixture_file('5k.png'), 'rb') @bad_file = File.new(fixture_file('bad.png'), 'rb') @dummy.avatar = @file @dummy.valid? @dummy.save! @files_on_s3 = s3_files_for(@dummy.avatar) end after do @file.close @bad_file.close @files_on_s3.values.each(&:close) if @files_on_s3 end context 'assigning itself to a new model' do before do @d2 = Dummy.new @d2.avatar = @dummy.avatar @d2.save end it "has the same name as the old file" do assert_equal @d2.avatar.original_filename, @dummy.avatar.original_filename end end it "has the same contents as the original" do assert_equal @file.read, @files_on_s3[:original].read end it "writes and delete its files" do [["434x66", :original], ["300x46", :large], ["100x15", :medium], ["32x32", :thumb]].each do |geo, style| cmd = %Q[identify -format "%wx%h" "#{@files_on_s3[style].path}"] assert_equal geo, `#{cmd}`.chomp, cmd end @d2 = Dummy.find(@dummy.id) @d2_files = s3_files_for @d2.avatar [["434x66", :original], ["300x46", :large], ["100x15", :medium], ["32x32", :thumb]].each do |geo, style| cmd = %Q[identify -format "%wx%h" "#{@d2_files[style].path}"] assert_equal geo, `#{cmd}`.chomp, cmd end @dummy.avatar.clear assert_nil @dummy.avatar_file_name assert @dummy.valid? assert @dummy.save [:thumb, :medium, :large, :original].each do |style| assert ! @dummy.avatar.exists?(style) end @d2 = Dummy.find(@dummy.id) assert_nil @d2.avatar_file_name end it "works exactly the same when new as when reloaded" do @d2 = Dummy.find(@dummy.id) assert_equal @dummy.avatar_file_name, @d2.avatar_file_name [:thumb, :medium, :large, :original].each do |style| begin first_file = open(@dummy.avatar.url(style)) second_file = open(@dummy.avatar.url(style)) assert_equal first_file.read, second_file.read ensure first_file.close if first_file second_file.close if second_file end end @d2.avatar.clear assert @d2.save [:thumb, :medium, :large, :original].each do |style| assert ! @dummy.avatar.exists?(style) end end it "knows the difference between good files, bad files, and nil" do @dummy.avatar = @bad_file assert ! @dummy.valid? @dummy.avatar = nil assert @dummy.valid? Dummy.validates_attachment_presence :avatar @d2 = Dummy.find(@dummy.id) @d2.avatar = @file assert @d2.valid? @d2.avatar = @bad_file assert ! @d2.valid? @d2.avatar = nil assert ! @d2.valid? end it "is able to reload without saving and not have the file disappear" do @dummy.avatar = @file assert @dummy.save @dummy.avatar = nil assert_nil @dummy.avatar_file_name @dummy.reload assert_equal "5k.png", @dummy.avatar_file_name end it "has the right content type" do headers = s3_headers_for(@dummy.avatar, :original) assert_equal 'image/png', headers['content-type'] end it "has the right style-specific headers" do headers = s3_headers_for(@dummy.avatar, :custom) assert_equal 'max-age=31557600', headers['cache-control'] end it "has the right style-specific metadata" do headers = s3_headers_for(@dummy.avatar, :custom) assert_equal 'bar', headers['x-amz-meta-foo'] end context "with non-english character in the file name" do before do @file.stubs(:original_filename).returns("クリップ.png") @dummy.avatar = @file end it "does not raise any error" do @dummy.save! end end end end context "Copying attachments between models" do before do rebuild_model @file = File.new(fixture_file("5k.png"), 'rb') end after { @file.close } it "succeeds when original attachment is a file" do original = Dummy.new original.avatar = @file assert original.save copy = Dummy.new copy.avatar = original.avatar assert copy.save assert copy.avatar.present? end it "succeeds when original attachment is empty" do original = Dummy.create! copy = Dummy.new copy.avatar = @file assert copy.save assert copy.avatar.present? copy.avatar = original.avatar assert copy.save assert !copy.avatar.present? end end end