├── .rubocop.yml ├── spec ├── support │ ├── fixtures │ │ ├── text.txt │ │ ├── empty.html │ │ ├── bad.png │ │ ├── 12k.png │ │ ├── 5k.png │ │ ├── 50x50.png │ │ ├── animated │ │ ├── animated.gif │ │ ├── empty.xlsx │ │ ├── rotated.jpg │ │ ├── twopage.pdf │ │ ├── uppercase.PNG │ │ ├── animated.unknown │ │ ├── spaced file.jpg │ │ ├── spaced file.png │ │ ├── s3.yml │ │ └── fog.yml │ ├── matchers │ │ ├── exist.rb │ │ ├── accept.rb │ │ └── have_column.rb │ ├── rails_helpers.rb │ ├── version_helper.rb │ ├── fake_rails.rb │ ├── test_data.rb │ ├── fake_model.rb │ ├── mock_attachment.rb │ ├── mock_interpolator.rb │ ├── mock_url_generator_builder.rb │ ├── model_reconstruction.rb │ └── assertions.rb ├── database.yml ├── paperclip │ ├── io_adapters │ │ ├── identity_adapter_spec.rb │ │ ├── empty_string_adapter_spec.rb │ │ ├── nil_adapter_spec.rb │ │ ├── registry_spec.rb │ │ ├── stringio_adapter_spec.rb │ │ ├── abstract_adapter_spec.rb │ │ ├── data_uri_adapter_spec.rb │ │ ├── http_url_proxy_adapter_spec.rb │ │ ├── uri_adapter_spec.rb │ │ ├── attachment_adapter_spec.rb │ │ └── file_adapter_spec.rb │ ├── filename_cleaner_spec.rb │ ├── attachment_definitions_spec.rb │ ├── matchers │ │ ├── have_attached_file_matcher_spec.rb │ │ ├── validate_attachment_presence_matcher_spec.rb │ │ ├── validate_attachment_size_matcher_spec.rb │ │ └── validate_attachment_content_type_matcher_spec.rb │ ├── meta_class_spec.rb │ ├── processor_spec.rb │ ├── rails_environment_spec.rb │ ├── file_command_content_type_detector_spec.rb │ ├── plural_cache_spec.rb │ ├── tempfile_factory_spec.rb │ ├── glue_spec.rb │ ├── geometry_detector_spec.rb │ ├── validators │ │ ├── media_type_spoof_detection_validator_spec.rb │ │ └── attachment_presence_validator_spec.rb │ ├── geometry_parser_spec.rb │ ├── content_type_detector_spec.rb │ ├── processor_helpers_spec.rb │ ├── storage │ │ └── filesystem_spec.rb │ ├── media_type_spoof_detector_spec.rb │ ├── attachment_processing_spec.rb │ ├── rake_spec.rb │ ├── paperclip_missing_attachment_styles_spec.rb │ └── attachment_registry_spec.rb └── spec_helper.rb ├── lib ├── paperclip │ ├── version.rb │ ├── storage.rb │ ├── io_adapters │ │ ├── identity_adapter.rb │ │ ├── http_url_proxy_adapter.rb │ │ ├── empty_string_adapter.rb │ │ ├── data_uri_adapter.rb │ │ ├── nil_adapter.rb │ │ ├── file_adapter.rb │ │ ├── registry.rb │ │ ├── stringio_adapter.rb │ │ ├── attachment_adapter.rb │ │ ├── uri_adapter.rb │ │ ├── uploaded_file_adapter.rb │ │ └── abstract_adapter.rb │ ├── filename_cleaner.rb │ ├── locales │ │ └── en.yml │ ├── rails_environment.rb │ ├── tempfile_factory.rb │ ├── interpolations │ │ └── plural_cache.rb │ ├── logger.rb │ ├── glue.rb │ ├── geometry_parser_factory.rb │ ├── railtie.rb │ ├── file_command_content_type_detector.rb │ ├── validators │ │ ├── attachment_file_type_ignorance_validator.rb │ │ ├── attachment_presence_validator.rb │ │ ├── media_type_spoof_detection_validator.rb │ │ ├── attachment_file_name_validator.rb │ │ ├── attachment_content_type_validator.rb │ │ └── attachment_size_validator.rb │ ├── callbacks.rb │ ├── errors.rb │ ├── geometry_detector_factory.rb │ ├── tempfile.rb │ ├── attachment_registry.rb │ ├── matchers │ │ ├── have_attached_file_matcher.rb │ │ ├── validate_attachment_presence_matcher.rb │ │ ├── validate_attachment_size_matcher.rb │ │ └── validate_attachment_content_type_matcher.rb │ ├── processor_helpers.rb │ ├── processor.rb │ ├── url_generator.rb │ ├── matchers.rb │ ├── helpers.rb │ ├── content_type_detector.rb │ ├── media_type_spoof_detector.rb │ ├── validators.rb │ ├── schema.rb │ ├── missing_attachment_styles.rb │ ├── has_attached_file.rb │ ├── storage │ │ └── filesystem.rb │ └── style.rb └── generators │ └── paperclip │ ├── USAGE │ ├── templates │ └── paperclip_migration.rb.erb │ └── paperclip_generator.rb ├── features ├── support │ ├── fixtures │ │ ├── gemfile.txt │ │ ├── boot_config.txt │ │ └── preinitializer.txt │ ├── env.rb │ ├── fakeweb.rb │ ├── selectors.rb │ ├── paths.rb │ ├── file_helpers.rb │ └── rails.rb ├── step_definitions │ ├── html_steps.rb │ ├── s3_steps.rb │ ├── web_steps.rb │ └── attachment_steps.rb ├── migration.feature ├── rake_tasks.feature └── basic_integration.feature ├── .gitignore ├── .travis.yml ├── cucumber └── paperclip_steps.rb ├── Gemfile ├── Appraisals ├── gemfiles ├── 4.2.awsv2.0.gemfile └── 4.2.awsv2.1.gemfile ├── UPGRADING ├── RELEASING.md ├── LICENSE ├── Rakefile ├── paperclip.gemspec └── CONTRIBUTING.md /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .hound.yml 2 | -------------------------------------------------------------------------------- /spec/support/fixtures/text.txt: -------------------------------------------------------------------------------- 1 | paperclip! 2 | -------------------------------------------------------------------------------- /spec/support/fixtures/empty.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/support/fixtures/bad.png: -------------------------------------------------------------------------------- 1 | This is not an image. 2 | -------------------------------------------------------------------------------- /spec/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: ":memory:" 4 | 5 | -------------------------------------------------------------------------------- /lib/paperclip/version.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | VERSION = "4.3.1" unless defined? Paperclip::VERSION 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/fixtures/12k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/12k.png -------------------------------------------------------------------------------- /spec/support/fixtures/5k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/5k.png -------------------------------------------------------------------------------- /spec/support/fixtures/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/50x50.png -------------------------------------------------------------------------------- /spec/support/fixtures/animated: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/animated -------------------------------------------------------------------------------- /spec/support/fixtures/animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/animated.gif -------------------------------------------------------------------------------- /spec/support/fixtures/empty.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/empty.xlsx -------------------------------------------------------------------------------- /spec/support/fixtures/rotated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/rotated.jpg -------------------------------------------------------------------------------- /spec/support/fixtures/twopage.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/twopage.pdf -------------------------------------------------------------------------------- /spec/support/fixtures/uppercase.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/uppercase.PNG -------------------------------------------------------------------------------- /lib/paperclip/storage.rb: -------------------------------------------------------------------------------- 1 | require "paperclip/storage/filesystem" 2 | require "paperclip/storage/fog" 3 | require "paperclip/storage/s3" 4 | -------------------------------------------------------------------------------- /spec/support/fixtures/animated.unknown: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/animated.unknown -------------------------------------------------------------------------------- /spec/support/fixtures/spaced file.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/spaced file.jpg -------------------------------------------------------------------------------- /spec/support/fixtures/spaced file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/paperclip/master/spec/support/fixtures/spaced file.png -------------------------------------------------------------------------------- /spec/support/matchers/exist.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :exist do |expected| 2 | match do |actual| 3 | File.exist?(actual) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /features/support/fixtures/gemfile.txt: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "rails", "RAILS_VERSION" 4 | gem "rdoc" 5 | gem "sqlite3", "1.3.8" 6 | -------------------------------------------------------------------------------- /spec/support/matchers/accept.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :accept do |expected| 2 | match do |actual| 3 | actual.matches?(expected) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/rails_helpers.rb: -------------------------------------------------------------------------------- 1 | module RailsHelpers 2 | module ClassMethods 3 | def using_protected_attributes? 4 | ActiveRecord::VERSION::MAJOR < 4 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/paperclip/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Explain the generator 3 | 4 | Example: 5 | rails generate paperclip Thing 6 | 7 | This will create: 8 | what/will/it/create 9 | -------------------------------------------------------------------------------- /spec/support/version_helper.rb: -------------------------------------------------------------------------------- 1 | module VersionHelper 2 | def active_support_version 3 | ActiveSupport::VERSION::STRING 4 | end 5 | 6 | def ruby_version 7 | RUBY_VERSION 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | .rvmrc 4 | .bundle 5 | tmp 6 | .DS_Store 7 | 8 | *.log 9 | 10 | public 11 | paperclip*.gem 12 | capybara*.html 13 | 14 | *.rbc 15 | .rbx 16 | 17 | *SPIKE* 18 | *emfile.lock 19 | tags 20 | -------------------------------------------------------------------------------- /spec/support/fixtures/s3.yml: -------------------------------------------------------------------------------- 1 | development: 2 | key: 54321 3 | production: 4 | key: 12345 5 | test: 6 | bucket: <%= ENV['S3_BUCKET'] %> 7 | access_key_id: <%= ENV['S3_KEY'] %> 8 | secret_access_key: <%= ENV['S3_SECRET'] %> 9 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | require 'capybara/cucumber' 3 | require 'rspec/matchers' 4 | 5 | $CUCUMBER=1 6 | 7 | World(RSpec::Matchers) 8 | 9 | Before do 10 | @aruba_timeout_seconds = 120 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/fake_rails.rb: -------------------------------------------------------------------------------- 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 13 | -------------------------------------------------------------------------------- /spec/support/fixtures/fog.yml: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.0 3 | - 2.1 4 | - 2.2 5 | 6 | script: "bundle exec rake clean spec cucumber" 7 | 8 | gemfile: 9 | - gemfiles/4.2.awsv2.1.gemfile 10 | - gemfiles/4.2.awsv2.0.gemfile 11 | 12 | sudo: false 13 | cache: bundler 14 | -------------------------------------------------------------------------------- /features/support/fakeweb.rb: -------------------------------------------------------------------------------- 1 | require 'fake_web' 2 | 3 | FakeWeb.allow_net_connect = false 4 | 5 | module FakeWeb 6 | class StubSocket 7 | def read_timeout=(_ignored) 8 | end 9 | 10 | def continue_timeout=(_ignored) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/identity_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::IdentityAdapter do 4 | it "responds to #new by returning the argument" do 5 | adapter = Paperclip::IdentityAdapter.new 6 | assert_equal :target, adapter.new(:target) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/identity_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class IdentityAdapter < AbstractAdapter 3 | def new(adapter) 4 | adapter 5 | end 6 | end 7 | end 8 | 9 | Paperclip.io_adapters.register Paperclip::IdentityAdapter.new do |target| 10 | Paperclip.io_adapters.registered?(target) 11 | end 12 | 13 | -------------------------------------------------------------------------------- /cucumber/paperclip_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I attach an? "([^\"]*)" "([^\"]*)" file to an? "([^\"]*)" on S3$/ do |attachment, extension, model| 2 | stub_paperclip_s3(model, attachment, extension) 3 | attach_file attachment, 4 | "features/support/paperclip/#{model.gsub(" ", "_").underscore}/#{attachment}.#{extension}" 5 | end 6 | 7 | -------------------------------------------------------------------------------- /spec/support/test_data.rb: -------------------------------------------------------------------------------- 1 | module TestData 2 | def attachment(options={}) 3 | Paperclip::Attachment.new(:avatar, FakeModel.new, options) 4 | end 5 | 6 | def stringy_file 7 | StringIO.new('.\n') 8 | end 9 | 10 | def fixture_file(filename) 11 | File.join(File.dirname(__FILE__), 'fixtures', filename) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /features/support/fixtures/boot_config.txt: -------------------------------------------------------------------------------- 1 | class Rails::Boot 2 | def run 3 | load_initializer 4 | 5 | Rails::Initializer.class_eval do 6 | def load_gems 7 | @bundler_loaded ||= Bundler.require :default, Rails.env 8 | end 9 | end 10 | 11 | Rails::Initializer.run(:set_load_path) 12 | end 13 | end 14 | 15 | Rails.boot! 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'sqlite3', '~> 1.3.8', :platforms => :ruby 6 | gem 'pry' 7 | 8 | # Hinting at development dependencies 9 | # Prevents bundler from taking a long-time to resolve 10 | group :development, :test do 11 | gem 'activerecord-import' 12 | gem 'mime-types' 13 | gem 'builder' 14 | gem 'rubocop', require: false 15 | end 16 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "4.2.awsv2.1" do 2 | gem "rails", "~> 4.2.0" 3 | gem "aws-sdk", "~> 2.1.0" 4 | 5 | group :development, :test do 6 | gem 'mime-types', '>= 1.16', '< 4' 7 | end 8 | end 9 | 10 | appraise "4.2.awsv2.0" do 11 | gem "rails", "~> 4.2.0" 12 | gem "aws-sdk", "~> 2.0.0" 13 | 14 | group :development, :test do 15 | gem 'mime-types', '>= 1.16', '< 4' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/http_url_proxy_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class HttpUrlProxyAdapter < UriAdapter 3 | 4 | REGEXP = /\Ahttps?:\/\// 5 | 6 | def initialize(target) 7 | super(URI(target)) 8 | end 9 | 10 | end 11 | end 12 | 13 | Paperclip.io_adapters.register Paperclip::HttpUrlProxyAdapter do |target| 14 | String === target && target =~ Paperclip::HttpUrlProxyAdapter::REGEXP 15 | end 16 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/empty_string_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class EmptyStringAdapter < AbstractAdapter 3 | def initialize(target) 4 | end 5 | 6 | def nil? 7 | false 8 | end 9 | 10 | def assignment? 11 | false 12 | end 13 | end 14 | end 15 | 16 | Paperclip.io_adapters.register Paperclip::EmptyStringAdapter do |target| 17 | target.is_a?(String) && target.empty? 18 | end 19 | -------------------------------------------------------------------------------- /lib/paperclip/filename_cleaner.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Paperclip 3 | class FilenameCleaner 4 | def initialize(invalid_character_regex) 5 | @invalid_character_regex = invalid_character_regex 6 | end 7 | 8 | def call(filename) 9 | if @invalid_character_regex 10 | filename.gsub(@invalid_character_regex, "_") 11 | else 12 | filename 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /gemfiles/4.2.awsv2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sqlite3", "~> 1.3.8", platforms: :ruby 6 | gem "pry" 7 | gem "rails", "~> 4.2.0" 8 | gem "aws-sdk", "~> 2.0.0" 9 | 10 | group :development, :test do 11 | gem "activerecord-import" 12 | gem "mime-types", ">= 1.16", "< 4" 13 | gem "builder" 14 | gem "rubocop", :require => false 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/4.2.awsv2.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sqlite3", "~> 1.3.8", platforms: :ruby 6 | gem "pry" 7 | gem "rails", "~> 4.2.0" 8 | gem "aws-sdk", "~> 2.1.0" 9 | 10 | group :development, :test do 11 | gem "activerecord-import" 12 | gem "mime-types", ">= 1.16", "< 4" 13 | gem "builder" 14 | gem "rubocop", :require => false 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /spec/support/fake_model.rb: -------------------------------------------------------------------------------- 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 26 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/empty_string_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::EmptyStringAdapter do 4 | context 'a new instance' do 5 | before do 6 | @subject = Paperclip.io_adapters.for('') 7 | end 8 | 9 | it "returns false for a call to nil?" do 10 | assert !@subject.nil? 11 | end 12 | 13 | it 'returns false for a call to assignment?' do 14 | assert !@subject.assignment? 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/paperclip/templates/paperclip_migration.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= migration_class_name %> < ActiveRecord::Migration 2 | def self.up 3 | change_table :<%= table_name %> do |t| 4 | <% attachment_names.each do |attachment| -%> 5 | t.attachment :<%= attachment %> 6 | <% end -%> 7 | end 8 | end 9 | 10 | def self.down 11 | <% attachment_names.each do |attachment| -%> 12 | remove_attachment :<%= table_name %>, :<%= attachment %> 13 | <% end -%> 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/paperclip/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | in_between: "must be in between %{min} and %{max}" 5 | spoofed_media_type: "has contents that are not what they are reported to be" 6 | 7 | number: 8 | human: 9 | storage_units: 10 | format: "%n %u" 11 | units: 12 | byte: 13 | one: "Byte" 14 | other: "Bytes" 15 | kb: "KB" 16 | mb: "MB" 17 | gb: "GB" 18 | tb: "TB" 19 | -------------------------------------------------------------------------------- /lib/paperclip/rails_environment.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class RailsEnvironment 3 | def self.get 4 | new.get 5 | end 6 | 7 | def get 8 | if rails_exists? && rails_environment_exists? 9 | Rails.env 10 | else 11 | nil 12 | end 13 | end 14 | 15 | private 16 | 17 | def rails_exists? 18 | Object.const_defined?(:Rails) 19 | end 20 | 21 | def rails_environment_exists? 22 | Rails.respond_to?(:env) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/paperclip/tempfile_factory.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class TempfileFactory 3 | 4 | def generate(name = random_name) 5 | @name = name 6 | file = Tempfile.new([basename, extension]) 7 | file.binmode 8 | file 9 | end 10 | 11 | def extension 12 | File.extname(@name) 13 | end 14 | 15 | def basename 16 | Digest::MD5.hexdigest(File.basename(@name, extension)) 17 | end 18 | 19 | def random_name 20 | SecureRandom.uuid 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/paperclip/filename_cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe Paperclip::FilenameCleaner do 5 | it 'converts invalid characters to underscores' do 6 | cleaner = Paperclip::FilenameCleaner.new(/[aeiou]/) 7 | expect(cleaner.call("baseball")).to eq "b_s_b_ll" 8 | end 9 | 10 | it 'does not convert anything if the character regex is nil' do 11 | cleaner = Paperclip::FilenameCleaner.new(nil) 12 | expect(cleaner.call("baseball")).to eq "baseball" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/paperclip/attachment_definitions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Attachment Definitions" do 4 | it 'returns all of the attachments on the class' do 5 | reset_class "Dummy" 6 | Dummy.has_attached_file :avatar, {path: "abc"} 7 | Dummy.has_attached_file :other_attachment, {url: "123"} 8 | Dummy.do_not_validate_attachment_file_type :avatar 9 | expected = {avatar: {path: "abc"}, other_attachment: {url: "123"}} 10 | 11 | expect(Dummy.attachment_definitions).to eq expected 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /features/support/selectors.rb: -------------------------------------------------------------------------------- 1 | module HtmlSelectorsHelpers 2 | # Maps a name to a selector. Used primarily by the 3 | # 4 | # When /^(.+) within (.+)$/ do |step, scope| 5 | # 6 | # step definitions in web_steps.rb 7 | # 8 | def selector_for(locator) 9 | case locator 10 | when "the page" 11 | "html > body" 12 | else 13 | raise "Can't find mapping from \"#{locator}\" to a selector.\n" + 14 | "Now, go and add a mapping in #{__FILE__}" 15 | end 16 | end 17 | end 18 | 19 | World(HtmlSelectorsHelpers) 20 | -------------------------------------------------------------------------------- /lib/paperclip/interpolations/plural_cache.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Interpolations 3 | class PluralCache 4 | def initialize 5 | @symbol_cache = {}.compare_by_identity 6 | @klass_cache = {}.compare_by_identity 7 | end 8 | 9 | def pluralize_symbol(symbol) 10 | @symbol_cache[symbol] ||= symbol.to_s.downcase.pluralize 11 | end 12 | 13 | def underscore_and_pluralize_class(klass) 14 | @klass_cache[klass] ||= klass.name.underscore.pluralize 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /features/step_definitions/html_steps.rb: -------------------------------------------------------------------------------- 1 | Then %r{I should see an image with a path of "([^"]*)"} do |path| 2 | expect(page).to have_css("img[src^='#{path}']") 3 | end 4 | 5 | Then %r{^the file at "([^"]*)" is the same as "([^"]*)"$} do |web_file, path| 6 | expected = IO.read(path) 7 | actual = if web_file.match %r{^https?://} 8 | Net::HTTP.get(URI.parse(web_file)) 9 | else 10 | visit(web_file) 11 | page.body 12 | end 13 | actual.force_encoding("UTF-8") if actual.respond_to?(:force_encoding) 14 | expect(actual).to eq(expected) 15 | end 16 | -------------------------------------------------------------------------------- /lib/paperclip/logger.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Logger 3 | # Log a paperclip-specific line. This will log to STDOUT 4 | # by default. Set Paperclip.options[:log] to false to turn off. 5 | def log message 6 | logger.info("[paperclip] #{message}") if logging? 7 | end 8 | 9 | def logger #:nodoc: 10 | @logger ||= options[:logger] || ::Logger.new(STDOUT) 11 | end 12 | 13 | def logger=(logger) 14 | @logger = logger 15 | end 16 | 17 | def logging? #:nodoc: 18 | options[:log] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/paperclip/glue.rb: -------------------------------------------------------------------------------- 1 | require 'paperclip/callbacks' 2 | require 'paperclip/validators' 3 | require 'paperclip/schema' 4 | 5 | module Paperclip 6 | module Glue 7 | def self.included(base) 8 | base.extend ClassMethods 9 | base.send :include, Callbacks 10 | base.send :include, Validators 11 | base.send :include, Schema if defined? ActiveRecord::Base 12 | 13 | locale_path = Dir.glob(File.dirname(__FILE__) + "/locales/*.{rb,yml}") 14 | I18n.load_path += locale_path unless I18n.load_path.include?(locale_path) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/mock_attachment.rb: -------------------------------------------------------------------------------- 1 | class MockAttachment 2 | attr_accessor :updated_at, :original_filename 3 | 4 | def initialize(options = {}) 5 | @model = options[:model] 6 | @responds_to_updated_at = options[:responds_to_updated_at] 7 | @updated_at = options[:updated_at] 8 | @original_filename = options[:original_filename] 9 | end 10 | 11 | def instance 12 | @model 13 | end 14 | 15 | def respond_to?(meth) 16 | if meth.to_s == "updated_at" 17 | @responds_to_updated_at || @updated_at 18 | else 19 | super 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /UPGRADING: -------------------------------------------------------------------------------- 1 | ################################################## 2 | # NOTE FOR UPGRADING FROM 4.3.0 OR EARLIER # 3 | ################################################## 4 | 5 | Paperclip is now compatible with aws-sdk >= 2.0.0. 6 | 7 | If you are using S3 storage, aws-sdk >= 2.0.0 requires you to make a few small 8 | changes: 9 | 10 | * You must set the `s3_region` 11 | * If you are explicitly setting permissions anywhere, such as in an initializer, 12 | note that the format of the permissions changed from using an underscore to 13 | using a hyphen. For example, `:public_read` needs to be changed to 14 | `public-read`. 15 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/data_uri_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class DataUriAdapter < StringioAdapter 3 | 4 | REGEXP = /\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m 5 | 6 | def initialize(target_uri) 7 | super(extract_target(target_uri)) 8 | end 9 | 10 | private 11 | 12 | def extract_target(uri) 13 | data_uri_parts = uri.match(REGEXP) || [] 14 | StringIO.new(Base64.decode64(data_uri_parts[2] || '')) 15 | end 16 | 17 | end 18 | end 19 | 20 | Paperclip.io_adapters.register Paperclip::DataUriAdapter do |target| 21 | String === target && target =~ Paperclip::DataUriAdapter::REGEXP 22 | end 23 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/nil_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class NilAdapter < AbstractAdapter 3 | def initialize(target) 4 | end 5 | 6 | def original_filename 7 | "" 8 | end 9 | 10 | def content_type 11 | "" 12 | end 13 | 14 | def size 15 | 0 16 | end 17 | 18 | def nil? 19 | true 20 | end 21 | 22 | def read(*args) 23 | nil 24 | end 25 | 26 | def eof? 27 | true 28 | end 29 | end 30 | end 31 | 32 | Paperclip.io_adapters.register Paperclip::NilAdapter do |target| 33 | target.nil? || ( (Paperclip::Attachment === target) && !target.present? ) 34 | end 35 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/nil_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::NilAdapter do 4 | context 'a new instance' do 5 | before do 6 | @subject = Paperclip.io_adapters.for(nil) 7 | end 8 | 9 | it "gets the right filename" do 10 | assert_equal "", @subject.original_filename 11 | end 12 | 13 | it "gets the content type" do 14 | assert_equal "", @subject.content_type 15 | end 16 | 17 | it "gets the file's size" do 18 | assert_equal 0, @subject.size 19 | end 20 | 21 | it "returns true for a call to nil?" do 22 | assert @subject.nil? 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing paperclip 2 | 3 | 1. Update `lib/paperclip/version.rb` file accordingly. 4 | 2. Update `NEWS` to reflect the changes since last release. 5 | 3. Commit changes. There shouldn’t be code changes, and thus CI doesn’t need to 6 | run, you can then add “[ci skip]” to the commit message. 7 | 4. Tag the release: `git tag -m 'vVERSION' vVERSION` 8 | 5. Push changes: `git push --tags` 9 | 6. Build and publish the gem: 10 | 11 | ```bash 12 | gem build paperclip.gemspec 13 | gem push paperclip-VERSION.gem 14 | ``` 15 | 16 | 7. Announce the new release, making sure to say “thank you” to the contributors 17 | who helped shape this version. 18 | -------------------------------------------------------------------------------- /spec/paperclip/matchers/have_attached_file_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'paperclip/matchers' 3 | 4 | describe Paperclip::Shoulda::Matchers::HaveAttachedFileMatcher do 5 | extend Paperclip::Shoulda::Matchers 6 | 7 | it "rejects the dummy class if it has no attachment" do 8 | reset_table "dummies" 9 | reset_class "Dummy" 10 | matcher = self.class.have_attached_file(:avatar) 11 | expect(matcher).to_not accept(Dummy) 12 | end 13 | 14 | it 'accepts the dummy class if it has an attachment' do 15 | rebuild_model 16 | matcher = self.class.have_attached_file(:avatar) 17 | expect(matcher).to accept(Dummy) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/mock_interpolator.rb: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /features/support/fixtures/preinitializer.txt: -------------------------------------------------------------------------------- 1 | begin 2 | require "rubygems" 3 | require "bundler" 4 | rescue LoadError 5 | raise "Could not load the bundler gem. Install it with `gem install bundler`." 6 | end 7 | 8 | if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24") 9 | raise RuntimeError, "Your bundler version is too old for Rails 2.3." + 10 | "Run `gem install bundler` to upgrade." 11 | end 12 | 13 | begin 14 | # Set up load paths for all bundled gems 15 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__) 16 | Bundler.setup 17 | rescue Bundler::GemNotFound 18 | raise RuntimeError, "Bundler couldn't find some gems." + 19 | "Did you run `bundle install`?" 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/matchers/have_column.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_column do |column_name| 2 | chain :with_default do |default| 3 | @default = default 4 | end 5 | 6 | match do |columns| 7 | column = columns.detect{|column| column.name == column_name } 8 | column && column.default.to_s == @default.to_s 9 | end 10 | 11 | failure_message_method = 12 | if RSpec::Version::STRING.to_i >= 3 13 | :failure_message 14 | else 15 | :failure_message_for_should 16 | end 17 | 18 | send(failure_message_method) do |columns| 19 | "expected to find '#{column_name}', " + 20 | "default '#{@default}' " + 21 | "in #{columns.map { |column| [column.name, column.default] }}" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/file_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class FileAdapter < AbstractAdapter 3 | def initialize(target) 4 | @target = target 5 | cache_current_values 6 | end 7 | 8 | private 9 | 10 | def cache_current_values 11 | self.original_filename = @target.original_filename if @target.respond_to?(:original_filename) 12 | self.original_filename ||= File.basename(@target.path) 13 | @tempfile = copy_to_tempfile(@target) 14 | @content_type = ContentTypeDetector.new(@target.path).detect 15 | @size = File.size(@target) 16 | end 17 | end 18 | end 19 | 20 | Paperclip.io_adapters.register Paperclip::FileAdapter do |target| 21 | File === target || Tempfile === target 22 | end 23 | -------------------------------------------------------------------------------- /features/support/paths.rb: -------------------------------------------------------------------------------- 1 | module NavigationHelpers 2 | # Maps a name to a path. Used by the 3 | # 4 | # When /^I go to (.+)$/ do |page_name| 5 | # 6 | # step definition in web_steps.rb 7 | # 8 | def path_to(page_name) 9 | case page_name 10 | 11 | when /the home\s?page/ 12 | '/' 13 | when /the new user page/ 14 | '/users/new' 15 | else 16 | begin 17 | page_name =~ /the (.*) page/ 18 | path_components = $1.split(/\s+/) 19 | self.send(path_components.push('path').join('_').to_sym) 20 | rescue Object => e 21 | raise "Can't find mapping from \"#{page_name}\" to a path.\n" + 22 | "Now, go and add a mapping in #{__FILE__}" 23 | end 24 | end 25 | end 26 | end 27 | 28 | World(NavigationHelpers) 29 | -------------------------------------------------------------------------------- /spec/support/mock_url_generator_builder.rb: -------------------------------------------------------------------------------- 1 | class MockUrlGeneratorBuilder 2 | def initializer 3 | end 4 | 5 | def new(attachment, attachment_options) 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 28 | -------------------------------------------------------------------------------- /features/step_definitions/s3_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I attach the file "([^"]*)" to "([^"]*)" on S3$/ do |file_path, field| 2 | definition = Paperclip::AttachmentRegistry.definitions_for(User)[field.downcase.to_sym] 3 | path = if defined?(::AWS) 4 | "https://paperclip.s3.amazonaws.com#{definition[:path]}" 5 | else 6 | "https://paperclip.s3-us-west-2.amazonaws.com#{definition[:path]}" 7 | end 8 | path.gsub!(':filename', File.basename(file_path)) 9 | path.gsub!(/:([^\/\.]+)/) do |match| 10 | "([^\/\.]+)" 11 | end 12 | FakeWeb.register_uri(:put, Regexp.new(path), :body => defined?(::AWS) ? "OK" : "") 13 | step "I attach the file \"#{file_path}\" to \"#{field}\"" 14 | end 15 | 16 | Then /^the file at "([^"]*)" should be uploaded to S3$/ do |url| 17 | FakeWeb.registered_uri?(:put, url) 18 | end 19 | -------------------------------------------------------------------------------- /lib/paperclip/geometry_parser_factory.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class GeometryParser 3 | FORMAT = /\b(\d*)x?(\d*)\b(?:,(\d?))?(\@\>|\>\@|[\>\<\#\@\%^!])?/i 4 | def initialize(string) 5 | @string = string 6 | end 7 | 8 | def make 9 | if match 10 | Geometry.new( 11 | :height => @height, 12 | :width => @width, 13 | :modifier => @modifier, 14 | :orientation => @orientation 15 | ) 16 | end 17 | end 18 | 19 | private 20 | 21 | def match 22 | if actual_match = @string && @string.match(FORMAT) 23 | @width = actual_match[1] 24 | @height = actual_match[2] 25 | @orientation = actual_match[3] 26 | @modifier = actual_match[4] 27 | end 28 | actual_match 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/paperclip/meta_class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Metaclasses' do 4 | context "A meta-class of dummy" do 5 | if active_support_version >= "4.1" || ruby_version < "2.1" 6 | before do 7 | rebuild_model 8 | reset_class("Dummy") 9 | end 10 | 11 | it "is able to use Paperclip like a normal class" do 12 | @dummy = Dummy.new 13 | 14 | assert_nothing_raised do 15 | rebuild_meta_class_of(@dummy) 16 | end 17 | end 18 | 19 | it "works like any other instance" do 20 | @dummy = Dummy.new 21 | rebuild_meta_class_of(@dummy) 22 | 23 | assert_nothing_raised do 24 | @dummy.avatar = File.new(fixture_file("5k.png"), 'rb') 25 | end 26 | assert @dummy.save 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/registry.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class AdapterRegistry 3 | class NoHandlerError < Paperclip::Error; end 4 | 5 | attr_reader :registered_handlers 6 | 7 | def initialize 8 | @registered_handlers = [] 9 | end 10 | 11 | def register(handler_class, &block) 12 | @registered_handlers << [block, handler_class] 13 | end 14 | 15 | def handler_for(target) 16 | @registered_handlers.each do |tester, handler| 17 | return handler if tester.call(target) 18 | end 19 | raise NoHandlerError.new("No handler found for #{target.inspect}") 20 | end 21 | 22 | def registered?(target) 23 | @registered_handlers.any? do |tester, handler| 24 | handler === target 25 | end 26 | end 27 | 28 | def for(target) 29 | handler_for(target).new(target) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/paperclip/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'paperclip' 2 | require 'paperclip/schema' 3 | 4 | module Paperclip 5 | require 'rails' 6 | 7 | class Railtie < Rails::Railtie 8 | initializer 'paperclip.insert_into_active_record' do |app| 9 | ActiveSupport.on_load :active_record do 10 | Paperclip::Railtie.insert 11 | end 12 | 13 | if app.config.respond_to?(:paperclip_defaults) 14 | Paperclip::Attachment.default_options.merge!(app.config.paperclip_defaults) 15 | end 16 | end 17 | 18 | rake_tasks { load "tasks/paperclip.rake" } 19 | end 20 | 21 | class Railtie 22 | def self.insert 23 | Paperclip.options[:logger] = Rails.logger 24 | 25 | if defined?(ActiveRecord) 26 | Paperclip.options[:logger] = ActiveRecord::Base.logger 27 | ActiveRecord::Base.send(:include, Paperclip::Glue) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/paperclip/file_command_content_type_detector.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class FileCommandContentTypeDetector 3 | SENSIBLE_DEFAULT = "application/octet-stream" 4 | 5 | def initialize(filename) 6 | @filename = filename 7 | end 8 | 9 | def detect 10 | type_from_file_command 11 | end 12 | 13 | private 14 | 15 | def type_from_file_command 16 | # On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist. 17 | type = begin 18 | Paperclip.run("file", "-b --mime :file", file: @filename) 19 | rescue Cocaine::CommandLineError => e 20 | Paperclip.log("Error while determining content type: #{e}") 21 | SENSIBLE_DEFAULT 22 | end 23 | 24 | if type.nil? || type.match(/\(.*?\)/) 25 | type = SENSIBLE_DEFAULT 26 | end 27 | type.split(/[:;\s]+/)[0] 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /features/support/file_helpers.rb: -------------------------------------------------------------------------------- 1 | module FileHelpers 2 | def append_to(path, contents) 3 | cd(".") do 4 | File.open(path, "a") do |file| 5 | file.puts 6 | file.puts contents 7 | end 8 | end 9 | end 10 | 11 | def append_to_gemfile(contents) 12 | append_to('Gemfile', contents) 13 | end 14 | 15 | def comment_out_gem_in_gemfile(gemname) 16 | cd(".") do 17 | gemfile = File.read("Gemfile") 18 | gemfile.sub!(/^(\s*)(gem\s*['"]#{gemname})/, "\\1# \\2") 19 | File.open("Gemfile", 'w'){ |file| file.write(gemfile) } 20 | end 21 | end 22 | 23 | def read_from_web(url) 24 | file = if url.match %r{^https?://} 25 | Net::HTTP.get(URI.parse(url)) 26 | else 27 | visit(url) 28 | page.source 29 | end 30 | file.force_encoding("UTF-8") if file.respond_to?(:force_encoding) 31 | end 32 | end 33 | 34 | World(FileHelpers) 35 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/stringio_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class StringioAdapter < AbstractAdapter 3 | def initialize(target) 4 | @target = target 5 | cache_current_values 6 | end 7 | 8 | attr_writer :content_type 9 | 10 | private 11 | 12 | def cache_current_values 13 | self.original_filename = @target.original_filename if @target.respond_to?(:original_filename) 14 | self.original_filename ||= "data" 15 | @tempfile = copy_to_tempfile(@target) 16 | @content_type = ContentTypeDetector.new(@tempfile.path).detect 17 | @size = @target.size 18 | end 19 | 20 | def copy_to_tempfile(source) 21 | while data = source.read(16*1024) 22 | destination.write(data) 23 | end 24 | destination.rewind 25 | destination 26 | end 27 | 28 | end 29 | end 30 | 31 | Paperclip.io_adapters.register Paperclip::StringioAdapter do |target| 32 | StringIO === target 33 | end 34 | -------------------------------------------------------------------------------- /spec/paperclip/processor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::Processor do 4 | it "instantiates and call #make when sent #make to the class" do 5 | processor = mock 6 | processor.expects(:make).with() 7 | Paperclip::Processor.expects(:new).with(:one, :two, :three).returns(processor) 8 | Paperclip::Processor.make(:one, :two, :three) 9 | end 10 | 11 | context "Calling #convert" do 12 | it "runs the convert command with Cocaine" do 13 | Paperclip.options[:log_command] = false 14 | Cocaine::CommandLine.expects(:new).with("convert", "stuff", {}).returns(stub(:run)) 15 | Paperclip::Processor.new('filename').convert("stuff") 16 | end 17 | end 18 | 19 | context "Calling #identify" do 20 | it "runs the identify command with Cocaine" do 21 | Paperclip.options[:log_command] = false 22 | Cocaine::CommandLine.expects(:new).with("identify", "stuff", {}).returns(stub(:run)) 23 | Paperclip::Processor.new('filename').identify("stuff") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/paperclip/rails_environment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::RailsEnvironment do 4 | 5 | it "returns nil when Rails isn't defined" do 6 | resetting_rails_to(nil) do 7 | expect(Paperclip::RailsEnvironment.get).to be_nil 8 | end 9 | end 10 | 11 | it "returns nil when Rails.env isn't defined" do 12 | resetting_rails_to({}) do 13 | expect(Paperclip::RailsEnvironment.get).to be_nil 14 | end 15 | end 16 | 17 | it "returns the value of Rails.env if it is set" do 18 | resetting_rails_to(OpenStruct.new(env: "foo")) do 19 | expect(Paperclip::RailsEnvironment.get).to eq "foo" 20 | end 21 | end 22 | 23 | def resetting_rails_to(new_value) 24 | begin 25 | previous_rails = Object.send(:remove_const, "Rails") 26 | Object.const_set("Rails", new_value) unless new_value.nil? 27 | yield 28 | ensure 29 | Object.send(:remove_const, "Rails") if Object.const_defined?("Rails") 30 | Object.const_set("Rails", previous_rails) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/paperclip/file_command_content_type_detector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::FileCommandContentTypeDetector do 4 | it 'returns a content type based on the content of the file' do 5 | tempfile = Tempfile.new("something") 6 | tempfile.write("This is a file.") 7 | tempfile.rewind 8 | 9 | assert_equal "text/plain", Paperclip::FileCommandContentTypeDetector.new(tempfile.path).detect 10 | 11 | tempfile.close 12 | end 13 | 14 | it 'returns a sensible default when the file command is missing' do 15 | Paperclip.stubs(:run).raises(Cocaine::CommandLineError.new) 16 | @filename = "/path/to/something" 17 | assert_equal "application/octet-stream", 18 | Paperclip::FileCommandContentTypeDetector.new(@filename).detect 19 | end 20 | 21 | it 'returns a sensible default on the odd chance that run returns nil' do 22 | Paperclip.stubs(:run).returns(nil) 23 | assert_equal "application/octet-stream", 24 | Paperclip::FileCommandContentTypeDetector.new("windows").detect 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::AttachmentRegistry do 4 | context "for" do 5 | before do 6 | class AdapterTest 7 | def initialize(target); end 8 | end 9 | @subject = Paperclip::AdapterRegistry.new 10 | @subject.register(AdapterTest){|t| Symbol === t } 11 | end 12 | 13 | it "returns the class registered for the adapted type" do 14 | assert_equal AdapterTest, @subject.for(:target).class 15 | end 16 | end 17 | 18 | context "registered?" do 19 | before do 20 | class AdapterTest 21 | def initialize(target); end 22 | end 23 | @subject = Paperclip::AdapterRegistry.new 24 | @subject.register(AdapterTest){|t| Symbol === t } 25 | end 26 | 27 | it "returns true when the class of this adapter has been registered" do 28 | assert @subject.registered?(AdapterTest.new(:target)) 29 | end 30 | 31 | it "returns false when the adapter has not been registered" do 32 | assert ! @subject.registered?(Object) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/paperclip/paperclip_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/active_record' 2 | 3 | class PaperclipGenerator < ActiveRecord::Generators::Base 4 | desc "Create a migration to add paperclip-specific fields to your model. " + 5 | "The NAME argument is the name of your model, and the following " + 6 | "arguments are the name of the attachments" 7 | 8 | argument :attachment_names, :required => true, :type => :array, :desc => "The names of the attachment(s) to add.", 9 | :banner => "attachment_one attachment_two attachment_three ..." 10 | 11 | def self.source_root 12 | @source_root ||= File.expand_path('../templates', __FILE__) 13 | end 14 | 15 | def generate_migration 16 | migration_template "paperclip_migration.rb.erb", "db/migrate/#{migration_file_name}" 17 | end 18 | 19 | def migration_name 20 | "add_attachment_#{attachment_names.join("_")}_to_#{name.underscore.pluralize}" 21 | end 22 | 23 | def migration_file_name 24 | "#{migration_name}.rb" 25 | end 26 | 27 | def migration_class_name 28 | migration_name.camelize 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/attachment_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class AttachmentAdapter < AbstractAdapter 3 | def initialize(target) 4 | @target, @style = case target 5 | when Paperclip::Attachment 6 | [target, :original] 7 | when Paperclip::Style 8 | [target.attachment, target.name] 9 | end 10 | 11 | cache_current_values 12 | end 13 | 14 | private 15 | 16 | def cache_current_values 17 | self.original_filename = @target.original_filename 18 | @content_type = @target.content_type 19 | @tempfile = copy_to_tempfile(@target) 20 | @size = @tempfile.size || @target.size 21 | end 22 | 23 | def copy_to_tempfile(source) 24 | if source.staged? 25 | FileUtils.cp(source.staged_path(@style), destination.path) 26 | else 27 | source.copy_to_local_file(@style, destination.path) 28 | end 29 | destination 30 | end 31 | end 32 | end 33 | 34 | Paperclip.io_adapters.register Paperclip::AttachmentAdapter do |target| 35 | Paperclip::Attachment === target || Paperclip::Style === target 36 | end 37 | -------------------------------------------------------------------------------- /spec/paperclip/plural_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Plural cache' do 4 | it 'caches pluralizations' do 5 | cache = Paperclip::Interpolations::PluralCache.new 6 | symbol = :box 7 | 8 | first = cache.pluralize_symbol(symbol) 9 | second = cache.pluralize_symbol(symbol) 10 | expect(first).to equal(second) 11 | end 12 | 13 | it 'caches pluralizations and underscores' do 14 | class BigBox ; end 15 | cache = Paperclip::Interpolations::PluralCache.new 16 | klass = BigBox 17 | 18 | first = cache.underscore_and_pluralize_class(klass) 19 | second = cache.underscore_and_pluralize_class(klass) 20 | expect(first).to equal(second) 21 | end 22 | 23 | it 'pluralizes words' do 24 | cache = Paperclip::Interpolations::PluralCache.new 25 | symbol = :box 26 | 27 | expect(cache.pluralize_symbol(symbol)).to eq("boxes") 28 | end 29 | 30 | it 'pluralizes and underscore class names' do 31 | class BigBox ; end 32 | cache = Paperclip::Interpolations::PluralCache.new 33 | klass = BigBox 34 | 35 | expect(cache.underscore_and_pluralize_class(klass)).to eq("big_boxes") 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/uri_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module Paperclip 4 | class UriAdapter < AbstractAdapter 5 | def initialize(target) 6 | @target = target 7 | @content = download_content 8 | cache_current_values 9 | @tempfile = copy_to_tempfile(@content) 10 | end 11 | 12 | attr_writer :content_type 13 | 14 | private 15 | 16 | def download_content 17 | open(@target) 18 | end 19 | 20 | def cache_current_values 21 | @original_filename = @target.path.split("/").last 22 | @original_filename ||= "index.html" 23 | self.original_filename = @original_filename.strip 24 | 25 | @content_type = @content.content_type if @content.respond_to?(:content_type) 26 | @content_type ||= "text/html" 27 | 28 | @size = @content.size 29 | end 30 | 31 | def copy_to_tempfile(src) 32 | while data = src.read(16*1024) 33 | destination.write(data) 34 | end 35 | src.close 36 | destination.rewind 37 | destination 38 | end 39 | end 40 | end 41 | 42 | Paperclip.io_adapters.register Paperclip::UriAdapter do |target| 43 | target.kind_of?(URI) 44 | end 45 | -------------------------------------------------------------------------------- /lib/paperclip/validators/attachment_file_type_ignorance_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validations/presence' 2 | 3 | module Paperclip 4 | module Validators 5 | class AttachmentFileTypeIgnoranceValidator < ActiveModel::EachValidator 6 | def validate_each(record, attribute, value) 7 | # This doesn't do anything. It's just to mark that you don't care about 8 | # the file_names or content_types of your incoming attachments. 9 | end 10 | 11 | def self.helper_method_name 12 | :do_not_validate_attachment_file_type 13 | end 14 | end 15 | 16 | module HelperMethods 17 | # Places ActiveModel validations on the presence of a file. 18 | # Options: 19 | # * +if+: A lambda or name of an instance method. Validation will only 20 | # be run if this lambda or method returns true. 21 | # * +unless+: Same as +if+ but validates if lambda or method returns false. 22 | def do_not_validate_attachment_file_type(*attr_names) 23 | options = _merge_attributes(attr_names) 24 | validates_with AttachmentFileTypeIgnoranceValidator, options.dup 25 | end 26 | end 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/paperclip/validators/attachment_presence_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validations/presence' 2 | 3 | module Paperclip 4 | module Validators 5 | class AttachmentPresenceValidator < ActiveModel::EachValidator 6 | def validate_each(record, attribute, value) 7 | if record.send("#{attribute}_file_name").blank? 8 | record.errors.add(attribute, :blank, options) 9 | end 10 | end 11 | 12 | def self.helper_method_name 13 | :validates_attachment_presence 14 | end 15 | end 16 | 17 | module HelperMethods 18 | # Places ActiveModel validations on the presence of a file. 19 | # Options: 20 | # * +if+: A lambda or name of an instance method. Validation will only 21 | # be run if this lambda or method returns true. 22 | # * +unless+: Same as +if+ but validates if lambda or method returns false. 23 | def validates_attachment_presence(*attr_names) 24 | options = _merge_attributes(attr_names) 25 | validates_with AttachmentPresenceValidator, options.dup 26 | validate_before_processing AttachmentPresenceValidator, options.dup 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | LICENSE 3 | 4 | The MIT License 5 | 6 | Copyright (c) 2008-2016 Jon Yurek and thoughtbot, inc. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /lib/paperclip/callbacks.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Callbacks 3 | def self.included(base) 4 | base.extend(Defining) 5 | base.send(:include, Running) 6 | end 7 | 8 | module Defining 9 | def define_paperclip_callbacks(*callbacks) 10 | define_callbacks(*[callbacks, {:terminator => callback_terminator}].flatten) 11 | callbacks.each do |callback| 12 | eval <<-end_callbacks 13 | def before_#{callback}(*args, &blk) 14 | set_callback(:#{callback}, :before, *args, &blk) 15 | end 16 | def after_#{callback}(*args, &blk) 17 | set_callback(:#{callback}, :after, *args, &blk) 18 | end 19 | end_callbacks 20 | end 21 | end 22 | 23 | private 24 | 25 | def callback_terminator 26 | if ::ActiveSupport::VERSION::STRING >= '4.1' 27 | lambda { |target, result| result == false } 28 | else 29 | 'result == false' 30 | end 31 | end 32 | end 33 | 34 | module Running 35 | def run_paperclip_callbacks(callback, &block) 36 | run_callbacks(callback, &block) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/paperclip/tempfile_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::TempfileFactory do 4 | it "is able to generate a tempfile with the right name" do 5 | file = subject.generate("omg.png") 6 | assert File.extname(file.path), "png" 7 | end 8 | 9 | it "is able to generate a tempfile with the right name with a tilde at the beginning" do 10 | file = subject.generate("~omg.png") 11 | assert File.extname(file.path), "png" 12 | end 13 | 14 | it "is able to generate a tempfile with the right name with a tilde at the end" do 15 | file = subject.generate("omg.png~") 16 | assert File.extname(file.path), "png" 17 | end 18 | 19 | it "is able to generate a tempfile from a file with a really long name" do 20 | filename = "#{"longfilename" * 100}.png" 21 | file = subject.generate(filename) 22 | assert File.extname(file.path), "png" 23 | end 24 | 25 | it 'is able to take nothing as a parameter and not error' do 26 | file = subject.generate 27 | assert File.exist?(file.path) 28 | end 29 | 30 | it "does not throw Errno::ENAMETOOLONG when it has a really long name" do 31 | expect { subject.generate("o" * 255) }.to_not raise_error 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/uploaded_file_adapter.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class UploadedFileAdapter < AbstractAdapter 3 | def initialize(target) 4 | @target = target 5 | cache_current_values 6 | 7 | if @target.respond_to?(:tempfile) 8 | @tempfile = copy_to_tempfile(@target.tempfile) 9 | else 10 | @tempfile = copy_to_tempfile(@target) 11 | end 12 | end 13 | 14 | class << self 15 | attr_accessor :content_type_detector 16 | end 17 | 18 | private 19 | 20 | def cache_current_values 21 | self.original_filename = @target.original_filename 22 | @content_type = determine_content_type 23 | @size = File.size(@target.path) 24 | end 25 | 26 | def content_type_detector 27 | self.class.content_type_detector 28 | end 29 | 30 | def determine_content_type 31 | content_type = @target.content_type.to_s.strip 32 | if content_type_detector 33 | content_type = content_type_detector.new(@target.path).detect 34 | end 35 | content_type 36 | end 37 | end 38 | end 39 | 40 | Paperclip.io_adapters.register Paperclip::UploadedFileAdapter do |target| 41 | target.class.name.include?("UploadedFile") 42 | end 43 | -------------------------------------------------------------------------------- /lib/paperclip/validators/media_type_spoof_detection_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validations/presence' 2 | 3 | module Paperclip 4 | module Validators 5 | class MediaTypeSpoofDetectionValidator < ActiveModel::EachValidator 6 | def validate_each(record, attribute, value) 7 | adapter = Paperclip.io_adapters.for(value) 8 | if Paperclip::MediaTypeSpoofDetector.using(adapter, value.original_filename, value.content_type).spoofed? 9 | record.errors.add(attribute, :spoofed_media_type) 10 | end 11 | end 12 | end 13 | 14 | module HelperMethods 15 | # Places ActiveModel validations on the presence of a file. 16 | # Options: 17 | # * +if+: A lambda or name of an instance method. Validation will only 18 | # be run if this lambda or method returns true. 19 | # * +unless+: Same as +if+ but validates if lambda or method returns false. 20 | def validates_media_type_spoof_detection(*attr_names) 21 | options = _merge_attributes(attr_names) 22 | validates_with MediaTypeSpoofDetectionValidator, options.dup 23 | validate_before_processing MediaTypeSpoofDetectionValidator, options.dup 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/paperclip/errors.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | # A base error class for Paperclip. Most of the error that will be thrown 3 | # from Paperclip will inherits from this class. 4 | class Error < StandardError 5 | end 6 | 7 | module Errors 8 | # Will be thrown when a storage method is not found. 9 | class StorageMethodNotFound < Paperclip::Error 10 | end 11 | 12 | # Will be thrown when a command or executable is not found. 13 | class CommandNotFoundError < Paperclip::Error 14 | end 15 | 16 | # Attachments require a content_type or file_name validator, 17 | # or to have explicitly opted out of them. 18 | class MissingRequiredValidatorError < Paperclip::Error 19 | end 20 | 21 | # Will be thrown when ImageMagic cannot determine the uploaded file's 22 | # metadata, usually this would mean the file is not an image. 23 | class NotIdentifiedByImageMagickError < Paperclip::Error 24 | end 25 | 26 | # Will be thrown if the interpolation is creating an infinite loop. If you 27 | # are creating an interpolator which might cause an infinite loop, you 28 | # should be throwing this error upon the infinite loop as well. 29 | class InfiniteInterpolationError < Paperclip::Error 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/paperclip/io_adapters/abstract_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module/delegation' 2 | 3 | module Paperclip 4 | class AbstractAdapter 5 | OS_RESTRICTED_CHARACTERS = %r{[/:]} 6 | 7 | attr_reader :content_type, :original_filename, :size 8 | delegate :binmode, :binmode?, :close, :close!, :closed?, :eof?, :path, :rewind, :unlink, :to => :@tempfile 9 | alias :length :size 10 | 11 | def fingerprint 12 | @fingerprint ||= Digest::MD5.file(path).to_s 13 | end 14 | 15 | def read(length = nil, buffer = nil) 16 | @tempfile.read(length, buffer) 17 | end 18 | 19 | def inspect 20 | "#{self.class}: #{self.original_filename}" 21 | end 22 | 23 | def original_filename=(new_filename) 24 | return unless new_filename 25 | @original_filename = new_filename.gsub(OS_RESTRICTED_CHARACTERS, "_") 26 | end 27 | 28 | def nil? 29 | false 30 | end 31 | 32 | def assignment? 33 | true 34 | end 35 | 36 | private 37 | 38 | def destination 39 | @destination ||= TempfileFactory.new.generate(@original_filename.to_s) 40 | end 41 | 42 | def copy_to_tempfile(src) 43 | FileUtils.cp(src.path, destination.path) 44 | destination 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/paperclip/glue_spec.rb: -------------------------------------------------------------------------------- 1 | # require "spec_helper" 2 | 3 | describe Paperclip::Glue do 4 | describe "when ActiveRecord does not exist" do 5 | before do 6 | ActiveRecordSaved = ActiveRecord 7 | Object.send :remove_const, "ActiveRecord" 8 | end 9 | 10 | after do 11 | ActiveRecord = ActiveRecordSaved 12 | Object.send :remove_const, "ActiveRecordSaved" 13 | end 14 | 15 | it "does not fail" do 16 | NonActiveRecordModel = Class.new 17 | NonActiveRecordModel.send :include, Paperclip::Glue 18 | Object.send :remove_const, "NonActiveRecordModel" 19 | end 20 | end 21 | 22 | describe "when ActiveRecord does exist" do 23 | before do 24 | if Object.const_defined?("ActiveRecord") 25 | @defined_active_record = false 26 | else 27 | ActiveRecord = :defined 28 | @defined_active_record = true 29 | end 30 | end 31 | 32 | after do 33 | if @defined_active_record 34 | Object.send :remove_const, "ActiveRecord" 35 | end 36 | end 37 | 38 | it "does not fail" do 39 | NonActiveRecordModel = Class.new 40 | NonActiveRecordModel.send :include, Paperclip::Glue 41 | Object.send :remove_const, "NonActiveRecordModel" 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/paperclip/geometry_detector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::GeometryDetector do 4 | it 'identifies an image and extract its dimensions' do 5 | Paperclip::GeometryParser.stubs(:new).with("434x66,").returns(stub(make: :correct)) 6 | file = fixture_file("5k.png") 7 | factory = Paperclip::GeometryDetector.new(file) 8 | 9 | output = factory.make 10 | 11 | expect(output).to eq :correct 12 | end 13 | 14 | it 'identifies an image and extract its dimensions and orientation' do 15 | Paperclip::GeometryParser.stubs(:new).with("300x200,6").returns(stub(make: :correct)) 16 | file = fixture_file("rotated.jpg") 17 | factory = Paperclip::GeometryDetector.new(file) 18 | 19 | output = factory.make 20 | 21 | expect(output).to eq :correct 22 | end 23 | 24 | it 'avoids reading EXIF orientation if so configured' do 25 | begin 26 | Paperclip.options[:use_exif_orientation] = false 27 | Paperclip::GeometryParser.stubs(:new).with("300x200,1").returns(stub(make: :correct)) 28 | file = fixture_file("rotated.jpg") 29 | factory = Paperclip::GeometryDetector.new(file) 30 | 31 | output = factory.make 32 | 33 | expect(output).to eq :correct 34 | ensure 35 | Paperclip.options[:use_exif_orientation] = true 36 | end 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'appraisal' 3 | require 'rspec/core/rake_task' 4 | require 'cucumber/rake/task' 5 | 6 | desc 'Default: run unit tests.' 7 | task :default => [:clean, :all] 8 | 9 | desc 'Test the paperclip plugin under all supported Rails versions.' 10 | task :all do |t| 11 | if ENV['BUNDLE_GEMFILE'] 12 | exec('rake spec cucumber') 13 | else 14 | exec("rm -f gemfiles/*.lock") 15 | Rake::Task["appraisal:gemfiles"].execute 16 | Rake::Task["appraisal:install"].execute 17 | exec('rake appraisal') 18 | end 19 | end 20 | 21 | desc 'Test the paperclip plugin.' 22 | RSpec::Core::RakeTask.new(:spec) 23 | 24 | desc 'Run integration test' 25 | Cucumber::Rake::Task.new do |t| 26 | t.cucumber_opts = %w{--format progress} 27 | end 28 | 29 | desc 'Start an IRB session with all necessary files required.' 30 | task :shell do |t| 31 | chdir File.dirname(__FILE__) 32 | exec 'irb -I lib/ -I lib/paperclip -r rubygems -r active_record -r tempfile -r init' 33 | end 34 | 35 | desc 'Clean up files.' 36 | task :clean do |t| 37 | FileUtils.rm_rf "doc" 38 | FileUtils.rm_rf "tmp" 39 | FileUtils.rm_rf "pkg" 40 | FileUtils.rm_rf "public" 41 | FileUtils.rm "test/debug.log" rescue nil 42 | FileUtils.rm "test/paperclip.db" rescue nil 43 | Dir.glob("paperclip-*.gem").each{|f| FileUtils.rm f } 44 | end 45 | -------------------------------------------------------------------------------- /lib/paperclip/geometry_detector_factory.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class GeometryDetector 3 | def initialize(file) 4 | @file = file 5 | raise_if_blank_file 6 | end 7 | 8 | def make 9 | geometry = GeometryParser.new(geometry_string.strip).make 10 | geometry || raise(Errors::NotIdentifiedByImageMagickError.new) 11 | end 12 | 13 | private 14 | 15 | def geometry_string 16 | begin 17 | orientation = Paperclip.options[:use_exif_orientation] ? 18 | "%[exif:orientation]" : "1" 19 | Paperclip.run( 20 | "identify", 21 | "-format '%wx%h,#{orientation}' :file", { 22 | :file => "#{path}[0]" 23 | }, { 24 | :swallow_stderr => true 25 | } 26 | ) 27 | rescue Cocaine::ExitStatusError 28 | "" 29 | rescue Cocaine::CommandNotFoundError => e 30 | raise_because_imagemagick_missing 31 | end 32 | end 33 | 34 | def path 35 | @file.respond_to?(:path) ? @file.path : @file 36 | end 37 | 38 | def raise_if_blank_file 39 | if path.blank? 40 | raise Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name") 41 | end 42 | end 43 | 44 | def raise_because_imagemagick_missing 45 | raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.") 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/paperclip/tempfile.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | # Overriding some implementation of Tempfile 3 | class Tempfile < ::Tempfile 4 | # Due to how ImageMagick handles its image format conversion and how 5 | # Tempfile handles its naming scheme, it is necessary to override how 6 | # Tempfile makes # its names so as to allow for file extensions. Idea 7 | # taken from the comments on this blog post: 8 | # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions 9 | # 10 | # This is Ruby 1.9.3's implementation. 11 | def make_tmpname(prefix_suffix, n) 12 | if RUBY_PLATFORM =~ /java/ 13 | case prefix_suffix 14 | when String 15 | prefix, suffix = prefix_suffix, '' 16 | when Array 17 | prefix, suffix = *prefix_suffix 18 | else 19 | raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" 20 | end 21 | 22 | t = Time.now.strftime("%y%m%d") 23 | path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}-#{n}#{suffix}" 24 | else 25 | super 26 | end 27 | end 28 | end 29 | 30 | module TempfileEncoding 31 | # This overrides Tempfile#binmode to make sure that the extenal encoding 32 | # for binary mode is ASCII-8BIT. This behavior is what's in CRuby, but not 33 | # in JRuby 34 | def binmode 35 | set_encoding('ASCII-8BIT') 36 | super 37 | end 38 | end 39 | end 40 | 41 | if RUBY_PLATFORM =~ /java/ 42 | ::Tempfile.send :include, Paperclip::TempfileEncoding 43 | end 44 | -------------------------------------------------------------------------------- /lib/paperclip/attachment_registry.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Paperclip 4 | class AttachmentRegistry 5 | include Singleton 6 | 7 | def self.register(klass, attachment_name, attachment_options) 8 | instance.register(klass, attachment_name, attachment_options) 9 | end 10 | 11 | def self.clear 12 | instance.clear 13 | end 14 | 15 | def self.names_for(klass) 16 | instance.names_for(klass) 17 | end 18 | 19 | def self.each_definition(&block) 20 | instance.each_definition(&block) 21 | end 22 | 23 | def self.definitions_for(klass) 24 | instance.definitions_for(klass) 25 | end 26 | 27 | def initialize 28 | clear 29 | end 30 | 31 | def register(klass, attachment_name, attachment_options) 32 | @attachments ||= {} 33 | @attachments[klass] ||= {} 34 | @attachments[klass][attachment_name] = attachment_options 35 | end 36 | 37 | def clear 38 | @attachments = Hash.new { |h,k| h[k] = {} } 39 | end 40 | 41 | def names_for(klass) 42 | @attachments[klass].keys 43 | end 44 | 45 | def each_definition 46 | @attachments.each do |klass, attachments| 47 | attachments.each do |name, options| 48 | yield klass, name, options 49 | end 50 | end 51 | end 52 | 53 | def definitions_for(klass) 54 | parent_classes = klass.ancestors.reverse 55 | parent_classes.each_with_object({}) do |ancestor, inherited_definitions| 56 | inherited_definitions.deep_merge! @attachments[ancestor] 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 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 'mocha/api' 8 | require 'bourne' 9 | require 'ostruct' 10 | require 'pathname' 11 | require 'activerecord-import' 12 | 13 | ROOT = Pathname(File.expand_path(File.join(File.dirname(__FILE__), '..'))) 14 | 15 | puts "Testing against version #{ActiveRecord::VERSION::STRING}" 16 | 17 | $LOAD_PATH << File.join(ROOT, 'lib') 18 | $LOAD_PATH << File.join(ROOT, 'lib', 'paperclip') 19 | require File.join(ROOT, 'lib', 'paperclip.rb') 20 | 21 | FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures") 22 | config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) 23 | ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") 24 | ActiveRecord::Base.establish_connection(config['test']) 25 | unless ActiveRecord::VERSION::STRING < "4.2" 26 | ActiveRecord::Base.raise_in_transactional_callbacks = true 27 | end 28 | Paperclip.options[:logger] = ActiveRecord::Base.logger 29 | 30 | Dir[File.join(ROOT, 'spec', 'support', '**', '*.rb')].each{|f| require f } 31 | 32 | Rails = FakeRails.new('test', Pathname.new(ROOT).join('tmp')) 33 | ActiveSupport::Deprecation.silenced = true 34 | 35 | RSpec.configure do |config| 36 | config.include Assertions 37 | config.include ModelReconstruction 38 | config.include TestData 39 | config.extend VersionHelper 40 | config.extend RailsHelpers::ClassMethods 41 | config.mock_framework = :mocha 42 | config.before(:all) do 43 | rebuild_model 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /features/support/rails.rb: -------------------------------------------------------------------------------- 1 | PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze 2 | APP_NAME = 'testapp'.freeze 3 | BUNDLE_ENV_VARS = %w(RUBYOPT BUNDLE_PATH BUNDLE_BIN_PATH BUNDLE_GEMFILE) 4 | ORIGINAL_BUNDLE_VARS = Hash[ENV.select{ |key,value| BUNDLE_ENV_VARS.include?(key) }] 5 | 6 | ENV['RAILS_ENV'] = 'test' 7 | 8 | Before do 9 | gemfile = ENV['BUNDLE_GEMFILE'].to_s 10 | ENV['BUNDLE_GEMFILE'] = File.join(Dir.pwd, gemfile) unless gemfile.start_with?(Dir.pwd) 11 | @framework_version = nil 12 | end 13 | 14 | After do 15 | ORIGINAL_BUNDLE_VARS.each_pair do |key, value| 16 | ENV[key] = value 17 | end 18 | end 19 | 20 | When /^I reset Bundler environment variable$/ do 21 | BUNDLE_ENV_VARS.each do |key| 22 | ENV[key] = nil 23 | end 24 | end 25 | 26 | module RailsCommandHelpers 27 | def framework_version?(version_string) 28 | framework_version =~ /^#{version_string}/ 29 | end 30 | 31 | def framework_version 32 | @framework_version ||= `rails -v`[/^Rails (.+)$/, 1] 33 | end 34 | 35 | def framework_major_version 36 | framework_version.split(".").first.to_i 37 | end 38 | 39 | def using_protected_attributes? 40 | framework_major_version < 4 41 | end 42 | 43 | def new_application_command 44 | "rails new" 45 | end 46 | 47 | def generator_command 48 | if framework_major_version >= 4 49 | "rails generate" 50 | else 51 | "script/rails generate" 52 | end 53 | end 54 | 55 | def runner_command 56 | if framework_major_version >= 4 57 | "rails runner" 58 | else 59 | "script/rails runner" 60 | end 61 | end 62 | end 63 | World(RailsCommandHelpers) 64 | -------------------------------------------------------------------------------- /lib/paperclip/matchers/have_attached_file_matcher.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Shoulda 3 | module Matchers 4 | # Ensures that the given instance or class has an attachment with the 5 | # given name. 6 | # 7 | # Example: 8 | # describe User do 9 | # it { should have_attached_file(:avatar) } 10 | # end 11 | def have_attached_file name 12 | HaveAttachedFileMatcher.new(name) 13 | end 14 | 15 | class HaveAttachedFileMatcher 16 | def initialize attachment_name 17 | @attachment_name = attachment_name 18 | end 19 | 20 | def matches? subject 21 | @subject = subject 22 | @subject = @subject.class unless Class === @subject 23 | responds? && has_column? 24 | end 25 | 26 | def failure_message 27 | "Should have an attachment named #{@attachment_name}" 28 | end 29 | 30 | def failure_message_when_negated 31 | "Should not have an attachment named #{@attachment_name}" 32 | end 33 | alias negative_failure_message failure_message_when_negated 34 | 35 | def description 36 | "have an attachment named #{@attachment_name}" 37 | end 38 | 39 | protected 40 | 41 | def responds? 42 | methods = @subject.instance_methods.map(&:to_s) 43 | methods.include?("#{@attachment_name}") && 44 | methods.include?("#{@attachment_name}=") && 45 | methods.include?("#{@attachment_name}?") 46 | end 47 | 48 | def has_column? 49 | @subject.column_names.include?("#{@attachment_name}_file_name") 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/paperclip/processor_helpers.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module ProcessorHelpers 3 | class NoSuchProcessor < StandardError; end 4 | 5 | def processor(name) #:nodoc: 6 | @known_processors ||= {} 7 | if @known_processors[name.to_s] 8 | @known_processors[name.to_s] 9 | else 10 | name = name.to_s.camelize 11 | load_processor(name) unless Paperclip.const_defined?(name) 12 | processor = Paperclip.const_get(name) 13 | @known_processors[name.to_s] = processor 14 | end 15 | end 16 | 17 | def load_processor(name) 18 | if defined?(Rails.root) && Rails.root 19 | filename = "#{name.to_s.underscore}.rb" 20 | directories = %w(lib/paperclip lib/paperclip_processors) 21 | 22 | required = directories.map do |directory| 23 | pathname = File.expand_path(Rails.root.join(directory, filename)) 24 | file_exists = File.exist?(pathname) 25 | require pathname if file_exists 26 | file_exists 27 | end 28 | 29 | raise LoadError, "Could not find the '#{name}' processor in any of these paths: #{directories.join(', ')}" unless required.any? 30 | end 31 | end 32 | 33 | def clear_processors! 34 | @known_processors.try(:clear) 35 | end 36 | 37 | # You can add your own processor via the Paperclip configuration. Normally 38 | # Paperclip will load all processors from the 39 | # Rails.root/lib/paperclip_processors directory, but here you can add any 40 | # existing class using this mechanism. 41 | # 42 | # Paperclip.configure do |c| 43 | # c.register_processor :watermarker, WatermarkingProcessor.new 44 | # end 45 | def register_processor(name, processor) 46 | @known_processors ||= {} 47 | @known_processors[name.to_s] = processor 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::Validators::MediaTypeSpoofDetectionValidator do 4 | before do 5 | rebuild_model 6 | @dummy = Dummy.new 7 | end 8 | 9 | def build_validator(options = {}) 10 | @validator = Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(options.merge( 11 | attributes: :avatar 12 | )) 13 | end 14 | 15 | it "is on the attachment without being explicitly added" do 16 | assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :media_type_spoof_detection } 17 | end 18 | 19 | it "is not on the attachment when explicitly rejected" do 20 | rebuild_model validate_media_type: false 21 | assert Dummy.validators_on(:avatar).none?{ |validator| validator.kind == :media_type_spoof_detection } 22 | end 23 | 24 | it "returns default error message for spoofed media type" do 25 | build_validator 26 | file = File.new(fixture_file("5k.png"), "rb") 27 | @dummy.avatar.assign(file) 28 | 29 | detector = mock("detector", :spoofed? => true) 30 | Paperclip::MediaTypeSpoofDetector.stubs(:using).returns(detector) 31 | @validator.validate(@dummy) 32 | 33 | assert_equal I18n.t("errors.messages.spoofed_media_type"), @dummy.errors[:avatar].first 34 | end 35 | 36 | it "runs when attachment is dirty" do 37 | build_validator 38 | file = File.new(fixture_file("5k.png"), "rb") 39 | @dummy.avatar.assign(file) 40 | Paperclip::MediaTypeSpoofDetector.stubs(:using).returns(stub(:spoofed? => false)) 41 | 42 | @dummy.valid? 43 | 44 | assert_received(Paperclip::MediaTypeSpoofDetector, :using){|e| e.once } 45 | end 46 | 47 | it "does not run when attachment is not dirty" do 48 | Paperclip::MediaTypeSpoofDetector.stubs(:using).never 49 | @dummy.valid? 50 | assert_received(Paperclip::MediaTypeSpoofDetector, :using){|e| e.never } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/paperclip/matchers/validate_attachment_presence_matcher.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Shoulda 3 | module Matchers 4 | # Ensures that the given instance or class validates the presence of the 5 | # given attachment. 6 | # 7 | # describe User do 8 | # it { should validate_attachment_presence(:avatar) } 9 | # end 10 | def validate_attachment_presence name 11 | ValidateAttachmentPresenceMatcher.new(name) 12 | end 13 | 14 | class ValidateAttachmentPresenceMatcher 15 | def initialize attachment_name 16 | @attachment_name = attachment_name 17 | end 18 | 19 | def matches? subject 20 | @subject = subject 21 | @subject = subject.new if subject.class == Class 22 | error_when_not_valid? && no_error_when_valid? 23 | end 24 | 25 | def failure_message 26 | "Attachment #{@attachment_name} should be required" 27 | end 28 | 29 | def failure_message_when_negated 30 | "Attachment #{@attachment_name} should not be required" 31 | end 32 | alias negative_failure_message failure_message_when_negated 33 | 34 | def description 35 | "require presence of attachment #{@attachment_name}" 36 | end 37 | 38 | protected 39 | 40 | def error_when_not_valid? 41 | @subject.send(@attachment_name).assign(nil) 42 | @subject.valid? 43 | @subject.errors[:"#{@attachment_name}"].present? 44 | end 45 | 46 | def no_error_when_valid? 47 | @file = StringIO.new(".") 48 | @subject.send(@attachment_name).assign(@file) 49 | @subject.valid? 50 | expected_message = [ 51 | @attachment_name.to_s.titleize, 52 | I18n.t(:blank, scope: [:errors, :messages]) 53 | ].join(' ') 54 | @subject.errors.full_messages.exclude?(expected_message) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/paperclip/processor.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | # Paperclip processors allow you to modify attached files when they are 3 | # attached in any way you are able. Paperclip itself uses command-line 4 | # programs for its included Thumbnail processor, but custom processors 5 | # are not required to follow suit. 6 | # 7 | # Processors are required to be defined inside the Paperclip module and 8 | # are also required to be a subclass of Paperclip::Processor. There is 9 | # only one method you *must* implement to properly be a subclass: 10 | # #make, but #initialize may also be of use. Both methods accept 3 11 | # arguments: the file that will be operated on (which is an instance of 12 | # File), a hash of options that were defined in has_attached_file's 13 | # style hash, and the Paperclip::Attachment itself. 14 | # 15 | # All #make needs to return is an instance of File (Tempfile is 16 | # acceptable) which contains the results of the processing. 17 | # 18 | # See Paperclip.run for more information about using command-line 19 | # utilities from within Processors. 20 | class Processor 21 | attr_accessor :file, :options, :attachment 22 | 23 | def initialize file, options = {}, attachment = nil 24 | @file = file 25 | @options = options 26 | @attachment = attachment 27 | end 28 | 29 | def make 30 | end 31 | 32 | def self.make file, options = {}, attachment = nil 33 | new(file, options, attachment).make 34 | end 35 | 36 | # The convert method runs the convert binary with the provided arguments. 37 | # See Paperclip.run for the available options. 38 | def convert(arguments = "", local_options = {}) 39 | Paperclip.run('convert', arguments, local_options) 40 | end 41 | 42 | # The identify method runs the identify binary with the provided arguments. 43 | # See Paperclip.run for the available options. 44 | def identify(arguments = "", local_options = {}) 45 | Paperclip.run('identify', arguments, local_options) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'paperclip/matchers' 3 | 4 | describe Paperclip::Shoulda::Matchers::ValidateAttachmentPresenceMatcher do 5 | extend Paperclip::Shoulda::Matchers 6 | 7 | before do 8 | reset_table("dummies") do |d| 9 | d.string :avatar_file_name 10 | end 11 | reset_class "Dummy" 12 | Dummy.has_attached_file :avatar 13 | Dummy.do_not_validate_attachment_file_type :avatar 14 | end 15 | 16 | it "rejects a class with no validation" do 17 | expect(matcher).to_not accept(Dummy) 18 | end 19 | 20 | it "accepts a class with a matching validation" do 21 | Dummy.validates_attachment_presence :avatar 22 | expect(matcher).to accept(Dummy) 23 | end 24 | 25 | it "accepts an instance with other attachment validations" do 26 | reset_table("dummies") do |d| 27 | d.string :avatar_file_name 28 | d.string :avatar_content_type 29 | end 30 | Dummy.class_eval do 31 | validates_attachment_presence :avatar 32 | validates_attachment_content_type :avatar, content_type: 'image/gif' 33 | end 34 | dummy = Dummy.new 35 | 36 | dummy.avatar = File.new fixture_file('5k.png') 37 | 38 | expect(matcher).to accept(dummy) 39 | end 40 | 41 | context "using an :if to control the validation" do 42 | before do 43 | Dummy.class_eval do 44 | validates_attachment_presence :avatar, if: :go 45 | attr_accessor :go 46 | end 47 | end 48 | 49 | it "runs the validation if the control is true" do 50 | dummy = Dummy.new 51 | dummy.avatar = nil 52 | dummy.go = true 53 | expect(matcher).to accept(dummy) 54 | end 55 | 56 | it "does not run the validation if the control is false" do 57 | dummy = Dummy.new 58 | dummy.avatar = nil 59 | dummy.go = false 60 | expect(matcher).to_not accept(dummy) 61 | end 62 | end 63 | 64 | private 65 | 66 | def matcher 67 | self.class.validate_attachment_presence(:avatar) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/paperclip/url_generator.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Paperclip 4 | class UrlGenerator 5 | def initialize(attachment, attachment_options) 6 | @attachment = attachment 7 | @attachment_options = attachment_options 8 | end 9 | 10 | def for(style_name, options) 11 | timestamp_as_needed( 12 | escape_url_as_needed( 13 | @attachment_options[:interpolator].interpolate(most_appropriate_url, @attachment, style_name), 14 | options 15 | ), options) 16 | end 17 | 18 | private 19 | 20 | # This method is all over the place. 21 | def default_url 22 | if @attachment_options[:default_url].respond_to?(:call) 23 | @attachment_options[:default_url].call(@attachment) 24 | elsif @attachment_options[:default_url].is_a?(Symbol) 25 | @attachment.instance.send(@attachment_options[:default_url]) 26 | else 27 | @attachment_options[:default_url] 28 | end 29 | end 30 | 31 | def most_appropriate_url 32 | if @attachment.original_filename.nil? 33 | default_url 34 | else 35 | @attachment_options[:url] 36 | end 37 | end 38 | 39 | def timestamp_as_needed(url, options) 40 | if options[:timestamp] && timestamp_possible? 41 | delimiter_char = url.match(/\?.+=/) ? '&' : '?' 42 | "#{url}#{delimiter_char}#{@attachment.updated_at.to_s}" 43 | else 44 | url 45 | end 46 | end 47 | 48 | def timestamp_possible? 49 | @attachment.respond_to?(:updated_at) && @attachment.updated_at.present? 50 | end 51 | 52 | def escape_url_as_needed(url, options) 53 | if options[:escape] 54 | escape_url(url) 55 | else 56 | url 57 | end 58 | end 59 | 60 | def escape_url(url) 61 | if url.respond_to?(:escape) 62 | url.escape 63 | else 64 | URI.escape(url).gsub(escape_regex){|m| "%#{m.ord.to_s(16).upcase}" } 65 | end 66 | end 67 | 68 | def escape_regex 69 | /[\?\(\)\[\]\+]/ 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/stringio_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::StringioAdapter do 4 | context "a new instance" do 5 | before do 6 | @contents = "abc123" 7 | @stringio = StringIO.new(@contents) 8 | @subject = Paperclip.io_adapters.for(@stringio) 9 | end 10 | 11 | it "returns a file name" do 12 | assert_equal "data", @subject.original_filename 13 | end 14 | 15 | it "returns a content type" do 16 | assert_equal "text/plain", @subject.content_type 17 | end 18 | 19 | it "returns the size of the data" do 20 | assert_equal 6, @subject.size 21 | end 22 | 23 | it "returns the length of the data" do 24 | assert_equal 6, @subject.length 25 | end 26 | 27 | it "generates an MD5 hash of the contents" do 28 | assert_equal Digest::MD5.hexdigest(@contents), @subject.fingerprint 29 | end 30 | 31 | it "generates correct fingerprint after read" do 32 | fingerprint = Digest::MD5.hexdigest(@subject.read) 33 | assert_equal fingerprint, @subject.fingerprint 34 | end 35 | 36 | it "generates same fingerprint" do 37 | assert_equal @subject.fingerprint, @subject.fingerprint 38 | end 39 | 40 | it "returns the data contained in the StringIO" do 41 | assert_equal "abc123", @subject.read 42 | end 43 | 44 | it 'accepts a content_type' do 45 | @subject.content_type = 'image/png' 46 | assert_equal 'image/png', @subject.content_type 47 | end 48 | 49 | it 'accepts an original_filename' do 50 | @subject.original_filename = 'image.png' 51 | assert_equal 'image.png', @subject.original_filename 52 | end 53 | 54 | it "does not generate filenames that include restricted characters" do 55 | @subject.original_filename = 'image:restricted.png' 56 | assert_equal 'image_restricted.png', @subject.original_filename 57 | end 58 | 59 | it "does not generate paths that include restricted characters" do 60 | @subject.original_filename = 'image:restricted.png' 61 | expect(@subject.path).to_not match(/:/) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/paperclip/geometry_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::GeometryParser do 4 | it 'identifies an image and extract its dimensions with no orientation' do 5 | Paperclip::Geometry.stubs(:new).with( 6 | height: '73', 7 | width: '434', 8 | modifier: nil, 9 | orientation: nil 10 | ).returns(:correct) 11 | factory = Paperclip::GeometryParser.new("434x73") 12 | 13 | output = factory.make 14 | 15 | assert_equal :correct, output 16 | end 17 | 18 | it 'identifies an image and extract its dimensions with an empty orientation' do 19 | Paperclip::Geometry.stubs(:new).with( 20 | height: '73', 21 | width: '434', 22 | modifier: nil, 23 | orientation: '' 24 | ).returns(:correct) 25 | factory = Paperclip::GeometryParser.new("434x73,") 26 | 27 | output = factory.make 28 | 29 | assert_equal :correct, output 30 | end 31 | 32 | it 'identifies an image and extract its dimensions and orientation' do 33 | Paperclip::Geometry.stubs(:new).with( 34 | height: '200', 35 | width: '300', 36 | modifier: nil, 37 | orientation: '6' 38 | ).returns(:correct) 39 | factory = Paperclip::GeometryParser.new("300x200,6") 40 | 41 | output = factory.make 42 | 43 | assert_equal :correct, output 44 | end 45 | 46 | it 'identifies an image and extract its dimensions and modifier' do 47 | Paperclip::Geometry.stubs(:new).with( 48 | height: '64', 49 | width: '64', 50 | modifier: '#', 51 | orientation: nil 52 | ).returns(:correct) 53 | factory = Paperclip::GeometryParser.new("64x64#") 54 | 55 | output = factory.make 56 | 57 | assert_equal :correct, output 58 | end 59 | 60 | it 'identifies an image and extract its dimensions, orientation, and modifier' do 61 | Paperclip::Geometry.stubs(:new).with( 62 | height: '50', 63 | width: '100', 64 | modifier: '>', 65 | orientation: '7' 66 | ).returns(:correct) 67 | factory = Paperclip::GeometryParser.new("100x50,7>") 68 | 69 | output = factory.make 70 | 71 | assert_equal :correct, output 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/support/model_reconstruction.rb: -------------------------------------------------------------------------------- 1 | module ModelReconstruction 2 | def reset_class class_name 3 | ActiveRecord::Base.send(:include, Paperclip::Glue) 4 | Object.send(:remove_const, class_name) rescue nil 5 | klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) 6 | 7 | klass.class_eval do 8 | include Paperclip::Glue 9 | end 10 | 11 | klass.reset_column_information 12 | klass.connection_pool.clear_table_cache!(klass.table_name) if klass.connection_pool.respond_to?(:clear_table_cache!) 13 | klass.connection.schema_cache.clear_table_cache!(klass.table_name) if klass.connection.respond_to?(:schema_cache) 14 | klass 15 | end 16 | 17 | def reset_table table_name, &block 18 | block ||= lambda { |table| true } 19 | ActiveRecord::Base.connection.create_table :dummies, {force: true}, &block 20 | end 21 | 22 | def modify_table table_name, &block 23 | ActiveRecord::Base.connection.change_table :dummies, &block 24 | end 25 | 26 | def rebuild_model options = {} 27 | ActiveRecord::Base.connection.create_table :dummies, force: true do |table| 28 | table.column :title, :string 29 | table.column :other, :string 30 | table.column :avatar_file_name, :string 31 | table.column :avatar_content_type, :string 32 | table.column :avatar_file_size, :integer 33 | table.column :avatar_updated_at, :datetime 34 | table.column :avatar_fingerprint, :string 35 | end 36 | rebuild_class options 37 | end 38 | 39 | def rebuild_class options = {} 40 | reset_class("Dummy").tap do |klass| 41 | klass.has_attached_file :avatar, options 42 | klass.do_not_validate_attachment_file_type :avatar 43 | Paperclip.reset_duplicate_clash_check! 44 | end 45 | end 46 | 47 | def rebuild_meta_class_of obj, options = {} 48 | meta_class_of(obj).tap do |metaklass| 49 | metaklass.has_attached_file :avatar, options 50 | metaklass.do_not_validate_attachment_file_type :avatar 51 | Paperclip.reset_duplicate_clash_check! 52 | end 53 | end 54 | 55 | def meta_class_of(obj) 56 | class << obj 57 | self 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/paperclip/content_type_detector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::ContentTypeDetector do 4 | it 'returns a meaningful content type for open xml spreadsheets' do 5 | file = File.new(fixture_file("empty.xlsx")) 6 | assert_equal "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 7 | Paperclip::ContentTypeDetector.new(file.path).detect 8 | end 9 | 10 | it 'gives a sensible default when the name is empty' do 11 | assert_equal "application/octet-stream", Paperclip::ContentTypeDetector.new("").detect 12 | end 13 | 14 | it 'returns the empty content type when the file is empty' do 15 | tempfile = Tempfile.new("empty") 16 | assert_equal "inode/x-empty", Paperclip::ContentTypeDetector.new(tempfile.path).detect 17 | tempfile.close 18 | end 19 | 20 | it 'returns content type of file if it is an acceptable type' do 21 | MIME::Types.stubs(:type_for).returns([MIME::Type.new('application/mp4'), MIME::Type.new('video/mp4'), MIME::Type.new('audio/mp4')]) 22 | Paperclip::ContentTypeDetector.any_instance 23 | .stubs(:type_from_file_contents).returns("video/mp4") 24 | @filename = "my_file.mp4" 25 | assert_equal "video/mp4", Paperclip::ContentTypeDetector.new(@filename).detect 26 | end 27 | 28 | it 'finds the right type in the list via the file command' do 29 | @filename = "#{Dir.tmpdir}/something.hahalolnotreal" 30 | File.open(@filename, "w+") do |file| 31 | file.puts "This is a text file." 32 | file.rewind 33 | assert_equal "text/plain", Paperclip::ContentTypeDetector.new(file.path).detect 34 | end 35 | FileUtils.rm @filename 36 | end 37 | 38 | it 'returns a sensible default if something is wrong, like the file is gone' do 39 | @filename = "/path/to/nothing" 40 | assert_equal "application/octet-stream", Paperclip::ContentTypeDetector.new(@filename).detect 41 | end 42 | 43 | it 'returns a sensible default when the file command is missing' do 44 | Paperclip.stubs(:run).raises(Cocaine::CommandLineError.new) 45 | @filename = "/path/to/something" 46 | assert_equal "application/octet-stream", Paperclip::ContentTypeDetector.new(@filename).detect 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/paperclip/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'paperclip/matchers/have_attached_file_matcher' 2 | require 'paperclip/matchers/validate_attachment_presence_matcher' 3 | require 'paperclip/matchers/validate_attachment_content_type_matcher' 4 | require 'paperclip/matchers/validate_attachment_size_matcher' 5 | 6 | module Paperclip 7 | module Shoulda 8 | # Provides RSpec-compatible & Test::Unit-compatible matchers for testing Paperclip attachments. 9 | # 10 | # *RSpec* 11 | # 12 | # In spec_helper.rb, you'll need to require the matchers: 13 | # 14 | # require "paperclip/matchers" 15 | # 16 | # And _include_ the module: 17 | # 18 | # RSpec.configure do |config| 19 | # config.include Paperclip::Shoulda::Matchers 20 | # end 21 | # 22 | # Example: 23 | # describe User do 24 | # it { should have_attached_file(:avatar) } 25 | # it { should validate_attachment_presence(:avatar) } 26 | # it { should validate_attachment_content_type(:avatar). 27 | # allowing('image/png', 'image/gif'). 28 | # rejecting('text/plain', 'text/xml') } 29 | # it { should validate_attachment_size(:avatar). 30 | # less_than(2.megabytes) } 31 | # end 32 | # 33 | # 34 | # *TestUnit* 35 | # 36 | # In test_helper.rb, you'll need to require the matchers as well: 37 | # 38 | # require "paperclip/matchers" 39 | # 40 | # And _extend_ the module: 41 | # 42 | # class ActiveSupport::TestCase 43 | # extend Paperclip::Shoulda::Matchers 44 | # 45 | # #...other initializers...# 46 | # end 47 | # 48 | # Example: 49 | # require 'test_helper' 50 | # 51 | # class UserTest < ActiveSupport::TestCase 52 | # should have_attached_file(:avatar) 53 | # should validate_attachment_presence(:avatar) 54 | # should validate_attachment_content_type(:avatar). 55 | # allowing('image/png', 'image/gif'). 56 | # rejecting('text/plain', 'text/xml') 57 | # should validate_attachment_size(:avatar). 58 | # less_than(2.megabytes) 59 | # end 60 | # 61 | module Matchers 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/paperclip/helpers.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Helpers 3 | def configure 4 | yield(self) if block_given? 5 | end 6 | 7 | def interpolates key, &block 8 | Paperclip::Interpolations[key] = block 9 | end 10 | 11 | # The run method takes the name of a binary to run, the arguments to that binary 12 | # and some options: 13 | # 14 | # :command_path -> A $PATH-like variable that defines where to look for the binary 15 | # on the filesystem. Colon-separated, just like $PATH. 16 | # 17 | # :expected_outcodes -> An array of integers that defines the expected exit codes 18 | # of the binary. Defaults to [0]. 19 | # 20 | # :log_command -> Log the command being run when set to true (defaults to true). 21 | # This will only log if logging in general is set to true as well. 22 | # 23 | # :swallow_stderr -> Set to true if you don't care what happens on STDERR. 24 | # 25 | def run(cmd, arguments = "", interpolation_values = {}, local_options = {}) 26 | command_path = options[:command_path] 27 | Cocaine::CommandLine.path = [Cocaine::CommandLine.path, command_path].flatten.compact.uniq 28 | if logging? && (options[:log_command] || local_options[:log_command]) 29 | local_options = local_options.merge(:logger => logger) 30 | end 31 | Cocaine::CommandLine.new(cmd, arguments, local_options).run(interpolation_values) 32 | end 33 | 34 | # Find all instances of the given Active Record model +klass+ with attachment +name+. 35 | # This method is used by the refresh rake tasks. 36 | def each_instance_with_attachment(klass, name) 37 | class_for(klass).unscoped.where("#{name}_file_name IS NOT NULL").find_each do |instance| 38 | yield(instance) 39 | end 40 | end 41 | 42 | def class_for(class_name) 43 | class_name.split('::').inject(Object) do |klass, partial_class_name| 44 | if klass.const_defined?(partial_class_name) 45 | klass.const_get(partial_class_name, false) 46 | else 47 | klass.const_missing(partial_class_name) 48 | end 49 | end 50 | end 51 | 52 | def reset_duplicate_clash_check! 53 | @names_url = nil 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/paperclip/validators/attachment_presence_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::Validators::AttachmentPresenceValidator do 4 | before do 5 | rebuild_model 6 | @dummy = Dummy.new 7 | end 8 | 9 | def build_validator(options={}) 10 | @validator = Paperclip::Validators::AttachmentPresenceValidator.new(options.merge( 11 | attributes: :avatar 12 | )) 13 | end 14 | 15 | context "nil attachment" do 16 | before do 17 | @dummy.avatar = nil 18 | end 19 | 20 | context "with default options" do 21 | before do 22 | build_validator 23 | @validator.validate(@dummy) 24 | end 25 | 26 | it "adds error on the attachment" do 27 | assert @dummy.errors[:avatar].present? 28 | end 29 | 30 | it "does not add an error on the file_name attribute" do 31 | assert @dummy.errors[:avatar_file_name].blank? 32 | end 33 | end 34 | 35 | context "with :if option" do 36 | context "returning true" do 37 | before do 38 | build_validator if: true 39 | @validator.validate(@dummy) 40 | end 41 | 42 | it "performs a validation" do 43 | assert @dummy.errors[:avatar].present? 44 | end 45 | end 46 | 47 | context "returning false" do 48 | before do 49 | build_validator if: false 50 | @validator.validate(@dummy) 51 | end 52 | 53 | it "performs a validation" do 54 | assert @dummy.errors[:avatar].present? 55 | end 56 | end 57 | end 58 | end 59 | 60 | context "with attachment" do 61 | before do 62 | build_validator 63 | @dummy.avatar = StringIO.new('.\n') 64 | @validator.validate(@dummy) 65 | end 66 | 67 | it "does not add error on the attachment" do 68 | assert @dummy.errors[:avatar].blank? 69 | end 70 | 71 | it "does not add an error on the file_name attribute" do 72 | assert @dummy.errors[:avatar_file_name].blank? 73 | end 74 | end 75 | 76 | context "using the helper" do 77 | before do 78 | Dummy.validates_attachment_presence :avatar 79 | end 80 | 81 | it "adds the validator to the class" do 82 | assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_presence } 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/support/assertions.rb: -------------------------------------------------------------------------------- 1 | module Assertions 2 | def assert(truthy, message = nil) 3 | expect(!!truthy).to(eq(true), message) 4 | end 5 | 6 | def assert_equal(expected, actual, message = nil) 7 | expect(actual).to(eq(expected), message) 8 | end 9 | 10 | def assert_not_equal(expected, actual, message = nil) 11 | expect(actual).to_not(eq(expected), message) 12 | end 13 | 14 | def assert_raises(exception_class, message = nil, &block) 15 | expect(&block).to raise_error(exception_class, message) 16 | end 17 | 18 | def assert_nothing_raised(&block) 19 | expect(&block).to_not raise_error 20 | end 21 | 22 | def assert_nil(thing) 23 | expect(thing).to be_nil 24 | end 25 | 26 | def assert_contains(haystack, needle) 27 | expect(haystack).to include(needle) 28 | end 29 | 30 | def assert_match(pattern, value) 31 | expect(value).to match(pattern) 32 | end 33 | 34 | def assert_no_match(pattern, value) 35 | expect(value).to_not match(pattern) 36 | end 37 | 38 | def assert_file_exists(path_to_file) 39 | expect(path_to_file).to exist 40 | end 41 | 42 | def assert_file_not_exists(path_to_file) 43 | expect(path_to_file).to_not exist 44 | end 45 | 46 | def assert_empty(object) 47 | expect(object).to be_empty 48 | end 49 | 50 | def assert_success_response(url) 51 | Net::HTTP.get_response(URI.parse(url)) do |response| 52 | assert_equal "200", response.code, 53 | "Expected HTTP response code 200, got #{response.code}" 54 | end 55 | end 56 | 57 | def assert_not_found_response(url) 58 | Net::HTTP.get_response(URI.parse(url)) do |response| 59 | assert_equal "404", response.code, 60 | "Expected HTTP response code 404, got #{response.code}" 61 | end 62 | end 63 | 64 | def assert_forbidden_response(url) 65 | Net::HTTP.get_response(URI.parse(url)) do |response| 66 | assert_equal "403", response.code, 67 | "Expected HTTP response code 403, got #{response.code}" 68 | end 69 | end 70 | 71 | def assert_frame_dimensions(range, frames) 72 | frames.each_with_index do |frame, frame_index| 73 | frame.split('x').each_with_index do |dimension, dimension_index | 74 | assert range.include?(dimension.to_i), "Frame #{frame_index}[#{dimension_index}] should have been within #{range.inspect}, but was #{dimension}" 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/abstract_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::AbstractAdapter do 4 | class TestAdapter < Paperclip::AbstractAdapter 5 | attr_accessor :tempfile 6 | 7 | def content_type 8 | Paperclip::ContentTypeDetector.new(path).detect 9 | end 10 | end 11 | 12 | context "content type from file contents" do 13 | before do 14 | @adapter = TestAdapter.new 15 | @adapter.stubs(:path).returns("image.png") 16 | Paperclip.stubs(:run).returns("image/png\n") 17 | Paperclip::ContentTypeDetector.any_instance.stubs(:type_from_mime_magic).returns("image/png") 18 | end 19 | 20 | it "returns the content type without newline" do 21 | assert_equal "image/png", @adapter.content_type 22 | end 23 | end 24 | 25 | context "nil?" do 26 | it "returns false" do 27 | assert !TestAdapter.new.nil? 28 | end 29 | end 30 | 31 | context "delegation" do 32 | before do 33 | @adapter = TestAdapter.new 34 | @adapter.tempfile = stub("Tempfile") 35 | end 36 | 37 | [:binmode, :binmode?, :close, :close!, :closed?, :eof?, :path, :rewind, :unlink].each do |method| 38 | it "delegates #{method} to @tempfile" do 39 | @adapter.tempfile.stubs(method) 40 | @adapter.public_send(method) 41 | assert_received @adapter.tempfile, method 42 | end 43 | end 44 | end 45 | 46 | it 'gets rid of slashes and colons in filenames' do 47 | @adapter = TestAdapter.new 48 | @adapter.original_filename = "awesome/file:name.png" 49 | 50 | assert_equal "awesome_file_name.png", @adapter.original_filename 51 | end 52 | 53 | it 'is an assignment' do 54 | assert TestAdapter.new.assignment? 55 | end 56 | 57 | it 'is not nil' do 58 | assert !TestAdapter.new.nil? 59 | end 60 | 61 | it "generates a destination filename with no original filename" do 62 | @adapter = TestAdapter.new 63 | expect(@adapter.send(:destination).path).to_not be_nil 64 | end 65 | 66 | it 'uses the original filename to generate the tempfile' do 67 | @adapter = TestAdapter.new 68 | @adapter.original_filename = "file.png" 69 | expect(@adapter.send(:destination).path).to end_with(".png") 70 | end 71 | 72 | context "#original_filename=" do 73 | it "should not fail with a nil original filename" do 74 | adapter = TestAdapter.new 75 | expect{ adapter.original_filename = nil }.not_to raise_error 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /paperclip.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path("../lib", __FILE__) 2 | require 'paperclip/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "paperclip" 6 | s.version = Paperclip::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.author = "Jon Yurek" 9 | s.email = ["jyurek@thoughtbot.com"] 10 | s.homepage = "https://github.com/thoughtbot/paperclip" 11 | s.summary = "File attachments as attributes for ActiveRecord" 12 | s.description = "Easy upload management for ActiveRecord" 13 | s.license = "MIT" 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | if File.exist?('UPGRADING') 21 | s.post_install_message = File.read("UPGRADING") 22 | end 23 | 24 | s.requirements << "ImageMagick" 25 | s.required_ruby_version = ">= 1.9.2" 26 | 27 | s.add_dependency('activemodel', '>= 3.2.0') 28 | s.add_dependency('activesupport', '>= 3.2.0') 29 | s.add_dependency('cocaine', '~> 0.5.5') 30 | s.add_dependency('mime-types') 31 | s.add_dependency('mimemagic', '~> 0.3.0') 32 | 33 | s.add_development_dependency('activerecord', '>= 3.2.0') 34 | s.add_development_dependency('shoulda') 35 | s.add_development_dependency('rspec', '~> 3.0') 36 | s.add_development_dependency('appraisal') 37 | s.add_development_dependency('mocha') 38 | s.add_development_dependency('aws-sdk', '>= 1.5.7', "< 3.0", *((0..33).to_a.collect{ |release_number| "!= 2.0.#{release_number}" })) 39 | s.add_development_dependency('bourne') 40 | s.add_development_dependency('cucumber', '~> 1.3.18') 41 | s.add_development_dependency('aruba', '~> 0.9.0') 42 | s.add_development_dependency('nokogiri') 43 | # Ruby version < 1.9.3 can't install capybara > 2.0.3. 44 | s.add_development_dependency('capybara') 45 | s.add_development_dependency('bundler') 46 | s.add_development_dependency('fog-aws') 47 | s.add_development_dependency('fog-local') 48 | s.add_development_dependency('launchy') 49 | s.add_development_dependency('rake') 50 | s.add_development_dependency('fakeweb') 51 | s.add_development_dependency('railties') 52 | s.add_development_dependency('actionmailer', '>= 3.2.0') 53 | s.add_development_dependency('generator_spec') 54 | s.add_development_dependency('timecop') 55 | end 56 | -------------------------------------------------------------------------------- /spec/paperclip/processor_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::ProcessorHelpers do 4 | describe '.load_processor' do 5 | context 'when the file exists in lib/paperclip' do 6 | it 'loads it correctly' do 7 | pathname = Pathname.new('my_app') 8 | main_path = 'main_path' 9 | alternate_path = 'alternate_path' 10 | 11 | Rails.stubs(:root).returns(pathname) 12 | File.expects(:expand_path).with(pathname.join('lib/paperclip', 'custom.rb')).returns(main_path) 13 | File.expects(:expand_path).with(pathname.join('lib/paperclip_processors', 'custom.rb')).returns(alternate_path) 14 | File.expects(:exist?).with(main_path).returns(true) 15 | File.expects(:exist?).with(alternate_path).returns(false) 16 | 17 | Paperclip.expects(:require).with(main_path) 18 | 19 | Paperclip.load_processor(:custom) 20 | end 21 | end 22 | 23 | context 'when the file exists in lib/paperclip_processors' do 24 | it 'loads it correctly' do 25 | pathname = Pathname.new('my_app') 26 | main_path = 'main_path' 27 | alternate_path = 'alternate_path' 28 | 29 | Rails.stubs(:root).returns(pathname) 30 | File.expects(:expand_path).with(pathname.join('lib/paperclip', 'custom.rb')).returns(main_path) 31 | File.expects(:expand_path).with(pathname.join('lib/paperclip_processors', 'custom.rb')).returns(alternate_path) 32 | File.expects(:exist?).with(main_path).returns(false) 33 | File.expects(:exist?).with(alternate_path).returns(true) 34 | 35 | Paperclip.expects(:require).with(alternate_path) 36 | 37 | Paperclip.load_processor(:custom) 38 | end 39 | end 40 | 41 | context 'when the file does not exist in lib/paperclip_processors' do 42 | it 'raises an error' do 43 | pathname = Pathname.new('my_app') 44 | main_path = 'main_path' 45 | alternate_path = 'alternate_path' 46 | 47 | Rails.stubs(:root).returns(pathname) 48 | File.stubs(:expand_path).with(pathname.join('lib/paperclip', 'custom.rb')).returns(main_path) 49 | File.stubs(:expand_path).with(pathname.join('lib/paperclip_processors', 'custom.rb')).returns(alternate_path) 50 | File.stubs(:exist?).with(main_path).returns(false) 51 | File.stubs(:exist?).with(alternate_path).returns(false) 52 | 53 | assert_raises(LoadError) { Paperclip.processor(:custom) } 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/paperclip/storage/filesystem_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::Storage::Filesystem do 4 | context "Filesystem" do 5 | context "normal file" do 6 | before do 7 | rebuild_model styles: { thumbnail: "25x25#" } 8 | @dummy = Dummy.create! 9 | 10 | @file = File.open(fixture_file('5k.png')) 11 | @dummy.avatar = @file 12 | end 13 | 14 | after { @file.close } 15 | 16 | it "allows file assignment" do 17 | assert @dummy.save 18 | end 19 | 20 | it "stores the original" do 21 | @dummy.save 22 | assert_file_exists(@dummy.avatar.path) 23 | end 24 | 25 | it "stores the thumbnail" do 26 | @dummy.save 27 | assert_file_exists(@dummy.avatar.path(:thumbnail)) 28 | end 29 | 30 | it "is rewinded after flush_writes" do 31 | @dummy.avatar.instance_eval "def after_flush_writes; end" 32 | 33 | files = @dummy.avatar.queued_for_write.values 34 | @dummy.save 35 | assert files.none?(&:eof?), "Expect all the files to be rewinded." 36 | end 37 | 38 | it "is removed after after_flush_writes" do 39 | paths = @dummy.avatar.queued_for_write.values.map(&:path) 40 | @dummy.save 41 | assert paths.none?{ |path| File.exist?(path) }, 42 | "Expect all the files to be deleted." 43 | end 44 | 45 | it 'copies the file to a known location with copy_to_local_file' do 46 | tempfile = Tempfile.new("known_location") 47 | @dummy.avatar.copy_to_local_file(:original, tempfile.path) 48 | tempfile.rewind 49 | assert_equal @file.read, tempfile.read 50 | tempfile.close 51 | end 52 | end 53 | 54 | context "with file that has space in file name" do 55 | before do 56 | rebuild_model styles: { thumbnail: "25x25#" } 57 | @dummy = Dummy.create! 58 | 59 | @file = File.open(fixture_file('spaced file.png')) 60 | @dummy.avatar = @file 61 | @dummy.save 62 | end 63 | 64 | after { @file.close } 65 | 66 | it "stores the file" do 67 | assert_file_exists(@dummy.avatar.path) 68 | end 69 | 70 | it "returns a replaced version for path" do 71 | assert_match /.+\/spaced_file\.png/, @dummy.avatar.path 72 | end 73 | 74 | it "returns a replaced version for url" do 75 | assert_match /.+\/spaced_file\.png/, @dummy.avatar.url 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/paperclip/content_type_detector.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class ContentTypeDetector 3 | # The content-type detection strategy is as follows: 4 | # 5 | # 1. Blank/Empty files: If there's no filepath or the file is empty, 6 | # provide a sensible default (application/octet-stream or inode/x-empty) 7 | # 8 | # 2. Calculated match: Return the first result that is found by both the 9 | # `file` command and MIME::Types. 10 | # 11 | # 3. Standard types: Return the first standard (without an x- prefix) entry 12 | # in MIME::Types 13 | # 14 | # 4. Experimental types: If there were no standard types in MIME::Types 15 | # list, try to return the first experimental one 16 | # 17 | # 5. Raw `file` command: Just use the output of the `file` command raw, or 18 | # a sensible default. This is cached from Step 2. 19 | 20 | EMPTY_TYPE = "inode/x-empty" 21 | SENSIBLE_DEFAULT = "application/octet-stream" 22 | 23 | def initialize(filepath) 24 | @filepath = filepath 25 | end 26 | 27 | # Returns a String describing the file's content type 28 | def detect 29 | if blank_name? 30 | SENSIBLE_DEFAULT 31 | elsif empty_file? 32 | EMPTY_TYPE 33 | elsif calculated_type_matches.any? 34 | calculated_type_matches.first 35 | else 36 | type_from_file_contents || SENSIBLE_DEFAULT 37 | end.to_s 38 | end 39 | 40 | private 41 | 42 | def blank_name? 43 | @filepath.nil? || @filepath.empty? 44 | end 45 | 46 | def empty_file? 47 | File.exist?(@filepath) && File.size(@filepath) == 0 48 | end 49 | 50 | alias :empty? :empty_file? 51 | 52 | def calculated_type_matches 53 | possible_types.select do |content_type| 54 | content_type == type_from_file_contents 55 | end 56 | end 57 | 58 | def possible_types 59 | MIME::Types.type_for(@filepath).collect(&:content_type) 60 | end 61 | 62 | def type_from_file_contents 63 | type_from_mime_magic || type_from_file_command 64 | rescue Errno::ENOENT => e 65 | Paperclip.log("Error while determining content type: #{e}") 66 | SENSIBLE_DEFAULT 67 | end 68 | 69 | def type_from_mime_magic 70 | @type_from_mime_magic ||= 71 | MimeMagic.by_magic(File.open(@filepath)).try(:type) 72 | end 73 | 74 | def type_from_file_command 75 | @type_from_file_command ||= 76 | FileCommandContentTypeDetector.new(@filepath).detect 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/paperclip/media_type_spoof_detector.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class MediaTypeSpoofDetector 3 | def self.using(file, name, content_type) 4 | new(file, name, content_type) 5 | end 6 | 7 | def initialize(file, name, content_type) 8 | @file = file 9 | @name = name 10 | @content_type = content_type || "" 11 | end 12 | 13 | def spoofed? 14 | if has_name? && has_extension? && media_type_mismatch? && mapping_override_mismatch? 15 | Paperclip.log("Content Type Spoof: Filename #{File.basename(@name)} (#{supplied_content_type} from Headers, #{content_types_from_name.map(&:to_s)} from Extension), content type discovered from file command: #{calculated_content_type}. See documentation to allow this combination.") 16 | true 17 | else 18 | false 19 | end 20 | end 21 | 22 | private 23 | 24 | def has_name? 25 | @name.present? 26 | end 27 | 28 | def has_extension? 29 | File.extname(@name).present? 30 | end 31 | 32 | def media_type_mismatch? 33 | supplied_type_mismatch? || calculated_type_mismatch? 34 | end 35 | 36 | def supplied_type_mismatch? 37 | supplied_media_type.present? && !media_types_from_name.include?(supplied_media_type) 38 | end 39 | 40 | def calculated_type_mismatch? 41 | !media_types_from_name.include?(calculated_media_type) 42 | end 43 | 44 | def mapping_override_mismatch? 45 | !Array(mapped_content_type).include?(calculated_content_type) 46 | end 47 | 48 | 49 | def supplied_content_type 50 | @content_type 51 | end 52 | 53 | def supplied_media_type 54 | @content_type.split("/").first 55 | end 56 | 57 | def content_types_from_name 58 | @content_types_from_name ||= MIME::Types.type_for(@name) 59 | end 60 | 61 | def media_types_from_name 62 | @media_types_from_name ||= content_types_from_name.collect(&:media_type) 63 | end 64 | 65 | def calculated_content_type 66 | @calculated_content_type ||= type_from_file_command.chomp 67 | end 68 | 69 | def calculated_media_type 70 | @calculated_media_type ||= calculated_content_type.split("/").first 71 | end 72 | 73 | def type_from_file_command 74 | begin 75 | Paperclip.run("file", "-b --mime :file", :file => @file.path).split(/[:;]\s+/).first 76 | rescue Cocaine::CommandLineError 77 | "" 78 | end 79 | end 80 | 81 | def mapped_content_type 82 | Paperclip.options[:content_type_mappings][filename_extension] 83 | end 84 | 85 | def filename_extension 86 | File.extname(@name.to_s.downcase).sub(/^\./, '').to_sym 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/data_uri_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::DataUriAdapter do 4 | after do 5 | if @subject 6 | @subject.close 7 | end 8 | end 9 | 10 | it 'allows a missing mime-type' do 11 | adapter = Paperclip.io_adapters.for("data:;base64,#{original_base64_content}") 12 | assert_equal Paperclip::DataUriAdapter, adapter.class 13 | end 14 | 15 | it 'alows mime type that has dot in it' do 16 | adapter = Paperclip.io_adapters.for("data:image/vnd.microsoft.icon;base64,#{original_base64_content}") 17 | assert_equal Paperclip::DataUriAdapter, adapter.class 18 | end 19 | 20 | context "a new instance" do 21 | before do 22 | @contents = "data:image/png;base64,#{original_base64_content}" 23 | @subject = Paperclip.io_adapters.for(@contents) 24 | end 25 | 26 | it "returns a nondescript file name" do 27 | assert_equal "data", @subject.original_filename 28 | end 29 | 30 | it "returns a content type" do 31 | assert_equal "image/png", @subject.content_type 32 | end 33 | 34 | it "returns the size of the data" do 35 | assert_equal 4456, @subject.size 36 | end 37 | 38 | it "generates a correct MD5 hash of the contents" do 39 | assert_equal( 40 | Digest::MD5.hexdigest(Base64.decode64(original_base64_content)), 41 | @subject.fingerprint 42 | ) 43 | end 44 | 45 | it "generates correct fingerprint after read" do 46 | fingerprint = Digest::MD5.hexdigest(@subject.read) 47 | assert_equal fingerprint, @subject.fingerprint 48 | end 49 | 50 | it "generates same fingerprint" do 51 | assert_equal @subject.fingerprint, @subject.fingerprint 52 | end 53 | 54 | it 'accepts a content_type' do 55 | @subject.content_type = 'image/png' 56 | assert_equal 'image/png', @subject.content_type 57 | end 58 | 59 | it 'accepts an original_filename' do 60 | @subject.original_filename = 'image.png' 61 | assert_equal 'image.png', @subject.original_filename 62 | end 63 | 64 | it "does not generate filenames that include restricted characters" do 65 | @subject.original_filename = 'image:restricted.png' 66 | assert_equal 'image_restricted.png', @subject.original_filename 67 | end 68 | 69 | it "does not generate paths that include restricted characters" do 70 | @subject.original_filename = 'image:restricted.png' 71 | expect(@subject.path).to_not match(/:/) 72 | end 73 | 74 | end 75 | 76 | def original_base64_content 77 | Base64.encode64(original_file_contents) 78 | end 79 | 80 | def original_file_contents 81 | @original_file_contents ||= File.read(fixture_file('5k.png')) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'paperclip/matchers' 3 | 4 | describe Paperclip::Shoulda::Matchers::ValidateAttachmentSizeMatcher do 5 | extend Paperclip::Shoulda::Matchers 6 | 7 | before do 8 | reset_table("dummies") do |d| 9 | d.string :avatar_file_name 10 | d.integer :avatar_file_size 11 | end 12 | reset_class "Dummy" 13 | Dummy.do_not_validate_attachment_file_type :avatar 14 | Dummy.has_attached_file :avatar 15 | end 16 | 17 | context "Limiting size" do 18 | it "rejects a class with no validation" do 19 | expect(matcher.in(256..1024)).to_not accept(Dummy) 20 | end 21 | 22 | it "rejects a class with a validation that's too high" do 23 | Dummy.validates_attachment_size :avatar, in: 256..2048 24 | expect(matcher.in(256..1024)).to_not accept(Dummy) 25 | end 26 | 27 | it "accepts a class with a validation that's too low" do 28 | Dummy.validates_attachment_size :avatar, in: 0..1024 29 | expect(matcher.in(256..1024)).to_not accept(Dummy) 30 | end 31 | 32 | it "accepts a class with a validation that matches" do 33 | Dummy.validates_attachment_size :avatar, in: 256..1024 34 | expect(matcher.in(256..1024)).to accept(Dummy) 35 | end 36 | end 37 | 38 | context "allowing anything" do 39 | it "given a class with an upper limit" do 40 | Dummy.validates_attachment_size :avatar, less_than: 1 41 | expect(matcher).to accept(Dummy) 42 | end 43 | 44 | it "given a class with a lower limit" do 45 | Dummy.validates_attachment_size :avatar, greater_than: 1 46 | expect(matcher).to accept(Dummy) 47 | end 48 | end 49 | 50 | context "using an :if to control the validation" do 51 | before do 52 | Dummy.class_eval do 53 | validates_attachment_size :avatar, greater_than: 1024, if: :go 54 | attr_accessor :go 55 | end 56 | end 57 | 58 | it "run the validation if the control is true" do 59 | dummy = Dummy.new 60 | dummy.go = true 61 | expect(matcher.greater_than(1024)).to accept(dummy) 62 | end 63 | 64 | it "not run the validation if the control is false" do 65 | dummy = Dummy.new 66 | dummy.go = false 67 | expect(matcher.greater_than(1024)).to_not accept(dummy) 68 | end 69 | end 70 | 71 | context "post processing" do 72 | before do 73 | Dummy.validates_attachment_size :avatar, greater_than: 1024 74 | end 75 | 76 | it "be skipped" do 77 | dummy = Dummy.new 78 | dummy.avatar.expects(:post_process).never 79 | expect(matcher.greater_than(1024)).to accept(dummy) 80 | end 81 | end 82 | 83 | private 84 | 85 | def matcher 86 | self.class.validate_attachment_size(:avatar) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /features/migration.feature: -------------------------------------------------------------------------------- 1 | Feature: Migration 2 | 3 | Background: 4 | Given I generate a new rails application 5 | And I write to "app/models/user.rb" with: 6 | """ 7 | class User < ActiveRecord::Base; end 8 | """ 9 | 10 | Scenario: Vintage syntax 11 | When I write to "db/migrate/01_add_attachment_to_users.rb" with: 12 | """ 13 | class AddAttachmentToUsers < ActiveRecord::Migration 14 | def self.up 15 | create_table :users do |t| 16 | t.has_attached_file :avatar 17 | end 18 | end 19 | 20 | def self.down 21 | drop_attached_file :users, :avatar 22 | end 23 | end 24 | """ 25 | And I run a migration 26 | Then I should have attachment columns for "avatar" 27 | 28 | When I rollback a migration 29 | Then I should not have attachment columns for "avatar" 30 | 31 | Scenario: New syntax with create_table 32 | When I write to "db/migrate/01_add_attachment_to_users.rb" with: 33 | """ 34 | class AddAttachmentToUsers < ActiveRecord::Migration 35 | def self.up 36 | create_table :users do |t| 37 | t.attachment :avatar 38 | end 39 | end 40 | end 41 | """ 42 | And I run a migration 43 | Then I should have attachment columns for "avatar" 44 | 45 | Scenario: New syntax outside of create_table 46 | When I write to "db/migrate/01_create_users.rb" with: 47 | """ 48 | class CreateUsers < ActiveRecord::Migration 49 | def self.up 50 | create_table :users 51 | end 52 | end 53 | """ 54 | And I write to "db/migrate/02_add_attachment_to_users.rb" with: 55 | """ 56 | class AddAttachmentToUsers < ActiveRecord::Migration 57 | def self.up 58 | add_attachment :users, :avatar 59 | end 60 | 61 | def self.down 62 | remove_attachment :users, :avatar 63 | end 64 | end 65 | """ 66 | And I run a migration 67 | Then I should have attachment columns for "avatar" 68 | 69 | When I rollback a migration 70 | Then I should not have attachment columns for "avatar" 71 | 72 | Scenario: Rails 3.2 change method 73 | Given I am using Rails newer than 3.1 74 | When I write to "db/migrate/01_create_users.rb" with: 75 | """ 76 | class CreateUsers < ActiveRecord::Migration 77 | def self.up 78 | create_table :users 79 | end 80 | end 81 | """ 82 | When I write to "db/migrate/02_add_attachment_to_users.rb" with: 83 | """ 84 | class AddAttachmentToUsers < ActiveRecord::Migration 85 | def change 86 | add_attachment :users, :avatar 87 | end 88 | end 89 | """ 90 | And I run a migration 91 | Then I should have attachment columns for "avatar" 92 | 93 | When I rollback a migration 94 | Then I should not have attachment columns for "avatar" 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We love pull requests from everyone. By participating in this project, you agree 5 | to abide by the thoughtbot [code of conduct]. 6 | 7 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 8 | 9 | Here's a quick guide for contributing: 10 | 11 | 1. Fork the repo. 12 | 13 | 2. Run the tests. We only take pull requests with passing tests, and it's great 14 | to know that you have a clean slate: `bundle && bundle exec rake` 15 | 16 | 3. Add a test for your change. Only refactoring and documentation changes 17 | require no new tests. If you are adding functionality or fixing a bug, we need 18 | a test! 19 | 20 | 4. Make the test pass. 21 | 22 | 5. Push to your fork and submit a pull request. 23 | 24 | At this point you're waiting on us. We like to at least comment on, if not 25 | accept, pull requests within seven business days (most of the work on Paperclip 26 | gets done on Fridays). We may suggest some changes or improvements or 27 | alternatives. 28 | 29 | Some things that will increase the chance that your pull request is accepted, 30 | taken straight from the Ruby on Rails guide: 31 | 32 | * Use Rails idioms and helpers 33 | * Include tests that fail without your code, and pass with it 34 | * Update the documentation, the surrounding one, examples elsewhere, guides, 35 | whatever is affected by your contribution 36 | 37 | Running Tests 38 | ------------- 39 | 40 | Paperclip uses [Appraisal](https://github.com/thoughtbot/appraisal) to aid 41 | testing against multiple version of Ruby on Rails. This helps us to make sure 42 | that Paperclip performs correctly with them. 43 | 44 | Paperclip also uses [RSpec](http://rspec.info) for its unit tests. If you submit 45 | tests that are not written for Cucumber or RSpec without a very good reason, you 46 | will be asked to rewrite them before we'll accept. 47 | 48 | ### Bootstrapping your test suite: 49 | 50 | bundle install 51 | bundle exec appraisal install 52 | 53 | This will install all the required gems that requires to test against each 54 | version of Rails, which defined in `gemfiles/*.gemfile`. 55 | 56 | ### To run a full test suite: 57 | 58 | bundle exec appraisal rake 59 | 60 | This will run RSpec and Cucumber against all version of Rails 61 | 62 | ### To run single Test::Unit or Cucumber test 63 | 64 | You need to specify a `BUNDLE_GEMFILE` pointing to the gemfile before running 65 | the normal test command: 66 | 67 | BUNDLE_GEMFILE=gemfiles/4.1.gemfile rspec spec/paperclip/attachment_spec.rb 68 | BUNDLE_GEMFILE=gemfiles/4.1.gemfile cucumber features/basic_integration.feature 69 | 70 | Syntax 71 | ------ 72 | 73 | * Two spaces, no tabs. 74 | * No trailing whitespace. Blank lines should not have any space. 75 | * Prefer &&/|| over and/or. 76 | * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 77 | * a = b and not a=b. 78 | * Follow the conventions you see used in the source already. 79 | 80 | And in case we didn't emphasize it enough: we love tests! 81 | -------------------------------------------------------------------------------- /lib/paperclip/validators/attachment_file_name_validator.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Validators 3 | class AttachmentFileNameValidator < ActiveModel::EachValidator 4 | def initialize(options) 5 | options[:allow_nil] = true unless options.has_key?(:allow_nil) 6 | super 7 | end 8 | 9 | def self.helper_method_name 10 | :validates_attachment_file_name 11 | end 12 | 13 | def validate_each(record, attribute, value) 14 | base_attribute = attribute.to_sym 15 | attribute = "#{attribute}_file_name".to_sym 16 | value = record.send :read_attribute_for_validation, attribute 17 | 18 | return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 19 | 20 | validate_whitelist(record, attribute, value) 21 | validate_blacklist(record, attribute, value) 22 | 23 | if record.errors.include? attribute 24 | record.errors[attribute].each do |error| 25 | record.errors.add base_attribute, error 26 | end 27 | end 28 | end 29 | 30 | def validate_whitelist(record, attribute, value) 31 | if allowed.present? && allowed.none? { |type| type === value } 32 | mark_invalid record, attribute, allowed 33 | end 34 | end 35 | 36 | def validate_blacklist(record, attribute, value) 37 | if forbidden.present? && forbidden.any? { |type| type === value } 38 | mark_invalid record, attribute, forbidden 39 | end 40 | end 41 | 42 | def mark_invalid(record, attribute, patterns) 43 | record.errors.add attribute, :invalid, options.merge(:names => patterns.join(', ')) 44 | end 45 | 46 | def allowed 47 | [options[:matches]].flatten.compact 48 | end 49 | 50 | def forbidden 51 | [options[:not]].flatten.compact 52 | end 53 | 54 | def check_validity! 55 | unless options.has_key?(:matches) || options.has_key?(:not) 56 | raise ArgumentError, "You must pass in either :matches or :not to the validator" 57 | end 58 | end 59 | end 60 | 61 | module HelperMethods 62 | # Places ActiveModel validations on the name of the file 63 | # assigned. The possible options are: 64 | # * +matches+: Allowed filename patterns as Regexps. Can be a single one 65 | # or an array. 66 | # * +not+: Forbidden file name patterns, specified the same was as +matches+. 67 | # * +message+: The message to display when the uploaded file has an invalid 68 | # name. 69 | # * +if+: A lambda or name of an instance method. Validation will only 70 | # be run is this lambda or method returns true. 71 | # * +unless+: Same as +if+ but validates if lambda or method returns false. 72 | def validates_attachment_file_name(*attr_names) 73 | options = _merge_attributes(attr_names) 74 | validates_with AttachmentFileNameValidator, options.dup 75 | validate_before_processing AttachmentFileNameValidator, options.dup 76 | end 77 | end 78 | end 79 | end 80 | 81 | -------------------------------------------------------------------------------- /spec/paperclip/media_type_spoof_detector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::MediaTypeSpoofDetector do 4 | it 'rejects a file that is named .html and identifies as PNG' do 5 | file = File.open(fixture_file("5k.png")) 6 | assert Paperclip::MediaTypeSpoofDetector.using(file, "5k.html", "image/png").spoofed? 7 | end 8 | 9 | it 'does not reject a file that is named .jpg and identifies as PNG' do 10 | file = File.open(fixture_file("5k.png")) 11 | assert ! Paperclip::MediaTypeSpoofDetector.using(file, "5k.jpg", "image/png").spoofed? 12 | end 13 | 14 | it 'does not reject a file that is named .html and identifies as HTML' do 15 | file = File.open(fixture_file("empty.html")) 16 | assert ! Paperclip::MediaTypeSpoofDetector.using(file, "empty.html", "text/html").spoofed? 17 | end 18 | 19 | it 'does not reject a file that does not have a name' do 20 | file = File.open(fixture_file("empty.html")) 21 | assert ! Paperclip::MediaTypeSpoofDetector.using(file, "", "text/html").spoofed? 22 | end 23 | 24 | it 'does not reject a file that does have an extension' do 25 | file = File.open(fixture_file("empty.html")) 26 | assert ! Paperclip::MediaTypeSpoofDetector.using(file, "data", "text/html").spoofed? 27 | end 28 | 29 | it 'does not reject when the supplied file is an IOAdapter' do 30 | adapter = Paperclip.io_adapters.for(File.new(fixture_file("5k.png"))) 31 | assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename, adapter.content_type).spoofed? 32 | end 33 | 34 | it 'does not reject when the extension => content_type is in :content_type_mappings' do 35 | begin 36 | Paperclip.options[:content_type_mappings] = { pem: "text/plain" } 37 | file = Tempfile.open(["test", ".PEM"]) 38 | file.puts "Certificate!" 39 | file.close 40 | adapter = Paperclip.io_adapters.for(File.new(file.path)); 41 | assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename, adapter.content_type).spoofed? 42 | ensure 43 | Paperclip.options[:content_type_mappings] = {} 44 | end 45 | end 46 | 47 | it "rejects a file if named .html and is as HTML, but we're told JPG" do 48 | file = File.open(fixture_file("empty.html")) 49 | assert Paperclip::MediaTypeSpoofDetector.using(file, "empty.html", "image/jpg").spoofed? 50 | end 51 | 52 | it "does not reject if content_type is empty but otherwise checks out" do 53 | file = File.open(fixture_file("empty.html")) 54 | assert ! Paperclip::MediaTypeSpoofDetector.using(file, "empty.html", "").spoofed? 55 | end 56 | 57 | it 'does allow array as :content_type_mappings' do 58 | begin 59 | Paperclip.options[:content_type_mappings] = { 60 | html: ['binary', 'text/html'] 61 | } 62 | file = File.open(fixture_file('empty.html')) 63 | spoofed = Paperclip::MediaTypeSpoofDetector 64 | .using(file, "empty.html", "text/html").spoofed? 65 | assert !spoofed 66 | ensure 67 | Paperclip.options[:content_type_mappings] = {} 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/paperclip/attachment_processing_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe 'Attachment Processing' do 5 | context 'using validates_attachment_content_type' do 6 | before do 7 | rebuild_class 8 | end 9 | 10 | it 'processes attachments given a valid assignment' do 11 | file = File.new(fixture_file("5k.png")) 12 | Dummy.validates_attachment_content_type :avatar, content_type: "image/png" 13 | instance = Dummy.new 14 | attachment = instance.avatar 15 | attachment.expects(:post_process_styles) 16 | 17 | attachment.assign(file) 18 | end 19 | 20 | it 'does not process attachments given an invalid assignment with :not' do 21 | file = File.new(fixture_file("5k.png")) 22 | Dummy.validates_attachment_content_type :avatar, not: "image/png" 23 | instance = Dummy.new 24 | attachment = instance.avatar 25 | attachment.expects(:post_process_styles).never 26 | 27 | attachment.assign(file) 28 | end 29 | 30 | it 'does not process attachments given an invalid assignment with :content_type' do 31 | file = File.new(fixture_file("5k.png")) 32 | Dummy.validates_attachment_content_type :avatar, content_type: "image/tiff" 33 | instance = Dummy.new 34 | attachment = instance.avatar 35 | attachment.expects(:post_process_styles).never 36 | 37 | attachment.assign(file) 38 | end 39 | 40 | it 'allows what would be an invalid assignment when validation :if clause returns false' do 41 | invalid_assignment = File.new(fixture_file("5k.png")) 42 | Dummy.validates_attachment_content_type :avatar, content_type: "image/tiff", if: lambda{false} 43 | instance = Dummy.new 44 | attachment = instance.avatar 45 | attachment.expects(:post_process_styles) 46 | 47 | attachment.assign(invalid_assignment) 48 | end 49 | end 50 | 51 | context 'using validates_attachment' do 52 | it 'processes attachments given a valid assignment' do 53 | file = File.new(fixture_file("5k.png")) 54 | Dummy.validates_attachment :avatar, content_type: {content_type: "image/png"} 55 | instance = Dummy.new 56 | attachment = instance.avatar 57 | attachment.expects(:post_process_styles) 58 | 59 | attachment.assign(file) 60 | end 61 | 62 | it 'does not process attachments given an invalid assignment with :not' do 63 | file = File.new(fixture_file("5k.png")) 64 | Dummy.validates_attachment :avatar, content_type: {not: "image/png"} 65 | instance = Dummy.new 66 | attachment = instance.avatar 67 | attachment.expects(:post_process_styles).never 68 | 69 | attachment.assign(file) 70 | end 71 | 72 | it 'does not process attachments given an invalid assignment with :content_type' do 73 | file = File.new(fixture_file("5k.png")) 74 | Dummy.validates_attachment :avatar, content_type: {content_type: "image/tiff"} 75 | instance = Dummy.new 76 | attachment = instance.avatar 77 | attachment.expects(:post_process_styles).never 78 | 79 | attachment.assign(file) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/paperclip/validators.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | require 'active_support/concern' 3 | require 'active_support/core_ext/array/wrap' 4 | require 'paperclip/validators/attachment_content_type_validator' 5 | require 'paperclip/validators/attachment_file_name_validator' 6 | require 'paperclip/validators/attachment_presence_validator' 7 | require 'paperclip/validators/attachment_size_validator' 8 | require 'paperclip/validators/media_type_spoof_detection_validator' 9 | require 'paperclip/validators/attachment_file_type_ignorance_validator' 10 | 11 | module Paperclip 12 | module Validators 13 | extend ActiveSupport::Concern 14 | 15 | included do 16 | extend HelperMethods 17 | include HelperMethods 18 | end 19 | 20 | ::Paperclip::REQUIRED_VALIDATORS = [AttachmentFileNameValidator, AttachmentContentTypeValidator, AttachmentFileTypeIgnoranceValidator] 21 | 22 | module ClassMethods 23 | # This method is a shortcut to validator classes that is in 24 | # "Attachment...Validator" format. It is almost the same thing as the 25 | # +validates+ method that shipped with Rails, but this is customized to 26 | # be using with attachment validators. This is helpful when you're using 27 | # multiple attachment validators on a single attachment. 28 | # 29 | # Example of using the validator: 30 | # 31 | # validates_attachment :avatar, :presence => true, 32 | # :content_type => { :content_type => "image/jpg" }, 33 | # :size => { :in => 0..10.kilobytes } 34 | # 35 | def validates_attachment(*attributes) 36 | options = attributes.extract_options!.dup 37 | 38 | Paperclip::Validators.constants.each do |constant| 39 | if constant.to_s =~ /\AAttachment(.+)Validator\Z/ 40 | validator_kind = $1.underscore.to_sym 41 | 42 | if options.has_key?(validator_kind) 43 | validator_options = options.delete(validator_kind) 44 | validator_options = {} if validator_options == true 45 | conditional_options = options.slice(:if, :unless) 46 | Array.wrap(validator_options).each do |local_options| 47 | method_name = Paperclip::Validators.const_get(constant.to_s).helper_method_name 48 | send(method_name, attributes, local_options.merge(conditional_options)) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | 55 | def validate_before_processing(validator_class, options) 56 | options = options.dup 57 | attributes = options.delete(:attributes) 58 | attributes.each do |attribute| 59 | options[:attributes] = [attribute] 60 | create_validating_before_filter(attribute, validator_class, options) 61 | end 62 | end 63 | 64 | def create_validating_before_filter(attribute, validator_class, options) 65 | if_clause = options.delete(:if) 66 | unless_clause = options.delete(:unless) 67 | send(:"before_#{attribute}_post_process", :if => if_clause, :unless => unless_clause) do |*args| 68 | validator_class.new(options.dup).validate(self) 69 | end 70 | end 71 | 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /features/rake_tasks.feature: -------------------------------------------------------------------------------- 1 | Feature: Rake tasks 2 | 3 | Background: 4 | Given I generate a new rails application 5 | And I run a rails generator to generate a "User" scaffold with "name:string" 6 | And I run a paperclip generator to add a paperclip "attachment" to the "User" model 7 | And I run a migration 8 | And I attach :attachment with: 9 | """ 10 | :path => ":rails_root/public/system/:attachment/:style/:filename" 11 | """ 12 | 13 | Scenario: Paperclip refresh thumbnails task 14 | When I modify my attachment definition to: 15 | """ 16 | has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename", 17 | :styles => { :medium => "200x200#" } 18 | """ 19 | And I upload the fixture "5k.png" 20 | Then the attachment "medium/5k.png" should have a dimension of 200x200 21 | When I modify my attachment definition to: 22 | """ 23 | has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename", 24 | :styles => { :medium => "100x100#" } 25 | """ 26 | When I successfully run `bundle exec rake paperclip:refresh:thumbnails CLASS=User --trace` 27 | Then the attachment "original/5k.png" should exist 28 | And the attachment "medium/5k.png" should have a dimension of 100x100 29 | 30 | Scenario: Paperclip refresh metadata task 31 | When I upload the fixture "5k.png" 32 | And I swap the attachment "original/5k.png" with the fixture "12k.png" 33 | And I successfully run `bundle exec rake paperclip:refresh:metadata CLASS=User --trace` 34 | Then the attachment should have the same content type as the fixture "12k.png" 35 | And the attachment should have the same file size as the fixture "12k.png" 36 | 37 | Scenario: Paperclip refresh missing styles task 38 | When I upload the fixture "5k.png" 39 | Then the attachment file "original/5k.png" should exist 40 | And the attachment file "medium/5k.png" should not exist 41 | When I modify my attachment definition to: 42 | """ 43 | has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename", 44 | :styles => { :medium => "200x200#" } 45 | """ 46 | When I successfully run `bundle exec rake paperclip:refresh:missing_styles --trace` 47 | Then the attachment file "original/5k.png" should exist 48 | And the attachment file "medium/5k.png" should exist 49 | 50 | Scenario: Paperclip clean task 51 | When I upload the fixture "5k.png" 52 | And I upload the fixture "12k.png" 53 | Then the attachment file "original/5k.png" should exist 54 | And the attachment file "original/12k.png" should exist 55 | When I modify my attachment definition to: 56 | """ 57 | has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename" 58 | validates_attachment_size :attachment, :less_than => 10.kilobytes 59 | """ 60 | And I successfully run `bundle exec rake paperclip:clean CLASS=User --trace` 61 | Then the attachment file "original/5k.png" should exist 62 | But the attachment file "original/12k.png" should not exist 63 | -------------------------------------------------------------------------------- /lib/paperclip/schema.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/deprecation' 2 | 3 | module Paperclip 4 | # Provides helper methods that can be used in migrations. 5 | module Schema 6 | COLUMNS = {:file_name => :string, 7 | :content_type => :string, 8 | :file_size => :integer, 9 | :updated_at => :datetime} 10 | 11 | def self.included(base) 12 | ActiveRecord::ConnectionAdapters::Table.send :include, TableDefinition 13 | ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TableDefinition 14 | ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Statements 15 | 16 | if defined?(ActiveRecord::Migration::CommandRecorder) # Rails 3.1+ 17 | ActiveRecord::Migration::CommandRecorder.send :include, CommandRecorder 18 | end 19 | end 20 | 21 | module Statements 22 | def add_attachment(table_name, *attachment_names) 23 | raise ArgumentError, "Please specify attachment name in your add_attachment call in your migration." if attachment_names.empty? 24 | 25 | options = attachment_names.extract_options! 26 | 27 | attachment_names.each do |attachment_name| 28 | COLUMNS.each_pair do |column_name, column_type| 29 | column_options = options.merge(options[column_name.to_sym] || {}) 30 | add_column(table_name, "#{attachment_name}_#{column_name}", column_type, column_options) 31 | end 32 | end 33 | end 34 | 35 | def remove_attachment(table_name, *attachment_names) 36 | raise ArgumentError, "Please specify attachment name in your remove_attachment call in your migration." if attachment_names.empty? 37 | 38 | options = attachment_names.extract_options! 39 | 40 | attachment_names.each do |attachment_name| 41 | COLUMNS.keys.each do |column_name| 42 | remove_column(table_name, "#{attachment_name}_#{column_name}") 43 | end 44 | end 45 | end 46 | 47 | def drop_attached_file(*args) 48 | ActiveSupport::Deprecation.warn "Method `drop_attached_file` in the migration has been deprecated and will be replaced by `remove_attachment`." 49 | remove_attachment(*args) 50 | end 51 | end 52 | 53 | module TableDefinition 54 | def attachment(*attachment_names) 55 | options = attachment_names.extract_options! 56 | attachment_names.each do |attachment_name| 57 | COLUMNS.each_pair do |column_name, column_type| 58 | column_options = options.merge(options[column_name.to_sym] || {}) 59 | column("#{attachment_name}_#{column_name}", column_type, column_options) 60 | end 61 | end 62 | end 63 | 64 | def has_attached_file(*attachment_names) 65 | ActiveSupport::Deprecation.warn "Method `t.has_attached_file` in the migration has been deprecated and will be replaced by `t.attachment`." 66 | attachment(*attachment_names) 67 | end 68 | end 69 | 70 | module CommandRecorder 71 | def add_attachment(*args) 72 | record(:add_attachment, args) 73 | end 74 | 75 | private 76 | 77 | def invert_add_attachment(args) 78 | [:remove_attachment, args] 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/paperclip/matchers/validate_attachment_size_matcher.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Shoulda 3 | module Matchers 4 | # Ensures that the given instance or class validates the size of the 5 | # given attachment as specified. 6 | # 7 | # Examples: 8 | # it { should validate_attachment_size(:avatar). 9 | # less_than(2.megabytes) } 10 | # it { should validate_attachment_size(:icon). 11 | # greater_than(1024) } 12 | # it { should validate_attachment_size(:icon). 13 | # in(0..100) } 14 | def validate_attachment_size name 15 | ValidateAttachmentSizeMatcher.new(name) 16 | end 17 | 18 | class ValidateAttachmentSizeMatcher 19 | def initialize attachment_name 20 | @attachment_name = attachment_name 21 | end 22 | 23 | def less_than size 24 | @high = size 25 | self 26 | end 27 | 28 | def greater_than size 29 | @low = size 30 | self 31 | end 32 | 33 | def in range 34 | @low, @high = range.first, range.last 35 | self 36 | end 37 | 38 | def matches? subject 39 | @subject = subject 40 | @subject = @subject.new if @subject.class == Class 41 | lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high? 42 | end 43 | 44 | def failure_message 45 | "Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes" 46 | end 47 | 48 | def failure_message_when_negated 49 | "Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes" 50 | end 51 | alias negative_failure_message failure_message_when_negated 52 | 53 | def description 54 | "validate the size of attachment #{@attachment_name}" 55 | end 56 | 57 | protected 58 | 59 | def override_method object, method, &replacement 60 | (class << object; self; end).class_eval do 61 | define_method(method, &replacement) 62 | end 63 | end 64 | 65 | def passes_validation_with_size(new_size) 66 | file = StringIO.new(".") 67 | override_method(file, :size){ new_size } 68 | override_method(file, :to_tempfile){ file } 69 | 70 | @subject.send(@attachment_name).post_processing = false 71 | @subject.send(@attachment_name).assign(file) 72 | @subject.valid? 73 | @subject.errors[:"#{@attachment_name}_file_size"].blank? 74 | ensure 75 | @subject.send(@attachment_name).post_processing = true 76 | end 77 | 78 | def lower_than_low? 79 | @low.nil? || !passes_validation_with_size(@low - 1) 80 | end 81 | 82 | def higher_than_low? 83 | @low.nil? || passes_validation_with_size(@low + 1) 84 | end 85 | 86 | def lower_than_high? 87 | @high.nil? || @high == Float::INFINITY || passes_validation_with_size(@high - 1) 88 | end 89 | 90 | def higher_than_high? 91 | @high.nil? || @high == Float::INFINITY || !passes_validation_with_size(@high + 1) 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/paperclip/missing_attachment_styles.rb: -------------------------------------------------------------------------------- 1 | require 'paperclip/attachment_registry' 2 | require 'set' 3 | 4 | module Paperclip 5 | class << self 6 | attr_writer :registered_attachments_styles_path 7 | def registered_attachments_styles_path 8 | @registered_attachments_styles_path ||= Rails.root.join('public/system/paperclip_attachments.yml').to_s 9 | end 10 | end 11 | 12 | # Get list of styles saved on previous deploy (running rake paperclip:refresh:missing_styles) 13 | def self.get_registered_attachments_styles 14 | YAML.load_file(Paperclip.registered_attachments_styles_path) 15 | rescue Errno::ENOENT 16 | nil 17 | end 18 | private_class_method :get_registered_attachments_styles 19 | 20 | def self.save_current_attachments_styles! 21 | File.open(Paperclip.registered_attachments_styles_path, 'w') do |f| 22 | YAML.dump(current_attachments_styles, f) 23 | end 24 | end 25 | 26 | # Returns hash with styles for all classes using Paperclip. 27 | # Unfortunately current version does not work with lambda styles:( 28 | # { 29 | # :User => {:avatar => [:small, :big]}, 30 | # :Book => { 31 | # :cover => [:thumb, :croppable]}, 32 | # :sample => [:thumb, :big]}, 33 | # } 34 | # } 35 | def self.current_attachments_styles 36 | Hash.new.tap do |current_styles| 37 | Paperclip::AttachmentRegistry.each_definition do |klass, attachment_name, attachment_attributes| 38 | # TODO: is it even possible to take into account Procs? 39 | next if attachment_attributes[:styles].kind_of?(Proc) 40 | attachment_attributes[:styles].try(:keys).try(:each) do |style_name| 41 | klass_sym = klass.to_s.to_sym 42 | current_styles[klass_sym] ||= Hash.new 43 | current_styles[klass_sym][attachment_name.to_sym] ||= Array.new 44 | current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym 45 | current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq! 46 | end 47 | end 48 | end 49 | end 50 | private_class_method :current_attachments_styles 51 | 52 | # Returns hash with styles missing from recent run of rake paperclip:refresh:missing_styles 53 | # { 54 | # :User => {:avatar => [:big]}, 55 | # :Book => { 56 | # :cover => [:croppable]}, 57 | # } 58 | # } 59 | def self.missing_attachments_styles 60 | current_styles = current_attachments_styles 61 | registered_styles = get_registered_attachments_styles 62 | 63 | Hash.new.tap do |missing_styles| 64 | current_styles.each do |klass, attachment_definitions| 65 | attachment_definitions.each do |attachment_name, styles| 66 | registered = registered_styles[klass][attachment_name] || [] rescue [] 67 | missed = styles - registered 68 | if missed.present? 69 | klass_sym = klass.to_s.to_sym 70 | missing_styles[klass_sym] ||= Hash.new 71 | missing_styles[klass_sym][attachment_name.to_sym] ||= Array.new 72 | missing_styles[klass_sym][attachment_name.to_sym].concat(missed.to_a) 73 | missing_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq! 74 | end 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::HttpUrlProxyAdapter do 4 | context "a new instance" do 5 | before do 6 | @open_return = StringIO.new("xxx") 7 | @open_return.stubs(:content_type).returns("image/png") 8 | Paperclip::HttpUrlProxyAdapter.any_instance.stubs(:download_content).returns(@open_return) 9 | @url = "http://thoughtbot.com/images/thoughtbot-logo.png" 10 | @subject = Paperclip.io_adapters.for(@url) 11 | end 12 | 13 | after do 14 | @subject.close 15 | end 16 | 17 | it "returns a file name" do 18 | assert_equal "thoughtbot-logo.png", @subject.original_filename 19 | end 20 | 21 | it 'closes open handle after reading' do 22 | assert_equal true, @open_return.closed? 23 | end 24 | 25 | it "returns a content type" do 26 | assert_equal "image/png", @subject.content_type 27 | end 28 | 29 | it "returns the size of the data" do 30 | assert_equal @open_return.size, @subject.size 31 | end 32 | 33 | it "generates an MD5 hash of the contents" do 34 | assert_equal Digest::MD5.hexdigest("xxx"), @subject.fingerprint 35 | end 36 | 37 | it "generates correct fingerprint after read" do 38 | fingerprint = Digest::MD5.hexdigest(@subject.read) 39 | assert_equal fingerprint, @subject.fingerprint 40 | end 41 | 42 | it "generates same fingerprint" do 43 | assert_equal @subject.fingerprint, @subject.fingerprint 44 | end 45 | 46 | it "returns the data contained in the StringIO" do 47 | assert_equal "xxx", @subject.read 48 | end 49 | 50 | it 'accepts a content_type' do 51 | @subject.content_type = 'image/png' 52 | assert_equal 'image/png', @subject.content_type 53 | end 54 | 55 | it 'accepts an original_filename' do 56 | @subject.original_filename = 'image.png' 57 | assert_equal 'image.png', @subject.original_filename 58 | end 59 | end 60 | 61 | context "a url with query params" do 62 | before do 63 | Paperclip::HttpUrlProxyAdapter.any_instance.stubs(:download_content).returns(StringIO.new("x")) 64 | @url = "https://github.com/thoughtbot/paperclip?file=test" 65 | @subject = Paperclip.io_adapters.for(@url) 66 | end 67 | 68 | after do 69 | @subject.close 70 | end 71 | 72 | it "returns a file name" do 73 | assert_equal "paperclip", @subject.original_filename 74 | end 75 | end 76 | 77 | context "a url with restricted characters in the filename" do 78 | before do 79 | Paperclip::HttpUrlProxyAdapter.any_instance.stubs(:download_content).returns(StringIO.new("x")) 80 | @url = "https://github.com/thoughtbot/paper:clip.jpg" 81 | @subject = Paperclip.io_adapters.for(@url) 82 | end 83 | 84 | after do 85 | begin 86 | @subject.close 87 | rescue Exception 88 | true 89 | end 90 | end 91 | 92 | it "does not generate filenames that include restricted characters" do 93 | assert_equal "paper_clip.jpg", @subject.original_filename 94 | end 95 | 96 | it "does not generate paths that include restricted characters" do 97 | expect(@subject.path).to_not match(/:/) 98 | end 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /lib/paperclip/has_attached_file.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | class HasAttachedFile 3 | def self.define_on(klass, name, options) 4 | new(klass, name, options).define 5 | end 6 | 7 | def initialize(klass, name, options) 8 | @klass = klass 9 | @name = name 10 | @options = options 11 | end 12 | 13 | def define 14 | define_flush_errors 15 | define_getters 16 | define_setter 17 | define_query 18 | register_new_attachment 19 | add_active_record_callbacks 20 | add_paperclip_callbacks 21 | add_required_validations 22 | end 23 | 24 | private 25 | 26 | def define_flush_errors 27 | @klass.send(:validates_each, @name) do |record, attr, value| 28 | attachment = record.send(@name) 29 | attachment.send(:flush_errors) 30 | end 31 | end 32 | 33 | def define_getters 34 | define_instance_getter 35 | define_class_getter 36 | end 37 | 38 | def define_instance_getter 39 | name = @name 40 | options = @options 41 | 42 | @klass.send :define_method, @name do |*args| 43 | ivar = "@attachment_#{name}" 44 | attachment = instance_variable_get(ivar) 45 | 46 | if attachment.nil? 47 | attachment = Attachment.new(name, self, options) 48 | instance_variable_set(ivar, attachment) 49 | end 50 | 51 | if args.length > 0 52 | attachment.to_s(args.first) 53 | else 54 | attachment 55 | end 56 | end 57 | end 58 | 59 | def define_class_getter 60 | @klass.extend(ClassMethods) 61 | end 62 | 63 | def define_setter 64 | name = @name 65 | @klass.send :define_method, "#{@name}=" do |file| 66 | send(name).assign(file) 67 | end 68 | end 69 | 70 | def define_query 71 | name = @name 72 | @klass.send :define_method, "#{@name}?" do 73 | send(name).file? 74 | end 75 | end 76 | 77 | def register_new_attachment 78 | Paperclip::AttachmentRegistry.register(@klass, @name, @options) 79 | end 80 | 81 | def add_required_validations 82 | options = Paperclip::Attachment.default_options.deep_merge(@options) 83 | if options[:validate_media_type] != false 84 | name = @name 85 | @klass.validates_media_type_spoof_detection name, 86 | :if => ->(instance){ instance.send(name).dirty? } 87 | end 88 | end 89 | 90 | def add_active_record_callbacks 91 | name = @name 92 | @klass.send(:after_save) { send(name).send(:save) } 93 | @klass.send(:before_destroy) { send(name).send(:queue_all_for_delete) } 94 | if @klass.respond_to?(:after_commit) 95 | @klass.send(:after_commit, on: :destroy) do 96 | send(name).send(:flush_deletes) 97 | end 98 | else 99 | @klass.send(:after_destroy) { send(name).send(:flush_deletes) } 100 | end 101 | end 102 | 103 | def add_paperclip_callbacks 104 | @klass.send( 105 | :define_paperclip_callbacks, 106 | :post_process, :"#{@name}_post_process") 107 | end 108 | 109 | module ClassMethods 110 | def attachment_definitions 111 | Paperclip::AttachmentRegistry.definitions_for(self) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /features/step_definitions/web_steps.rb: -------------------------------------------------------------------------------- 1 | # TL;DR: YOU SHOULD DELETE THIS FILE 2 | # 3 | # This file was generated by Cucumber-Rails and is only here to get you a head start 4 | # These step definitions are thin wrappers around the Capybara/Webrat API that lets you 5 | # visit pages, interact with widgets and make assertions about page content. 6 | # 7 | # If you use these step definitions as basis for your features you will quickly end up 8 | # with features that are: 9 | # 10 | # * Hard to maintain 11 | # * Verbose to read 12 | # 13 | # A much better approach is to write your own higher level step definitions, following 14 | # the advice in the following blog posts: 15 | # 16 | # * http://benmabey.com/2008/05/19/imperative-vs-declarative-scenarios-in-user-stories.html 17 | # * http://dannorth.net/2011/01/31/whose-domain-is-it-anyway/ 18 | # * http://elabs.se/blog/15-you-re-cuking-it-wrong 19 | # 20 | 21 | 22 | require 'uri' 23 | require 'cgi' 24 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths")) 25 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors")) 26 | 27 | module WithinHelpers 28 | def with_scope(locator) 29 | locator ? within(*selector_for(locator)) { yield } : yield 30 | end 31 | end 32 | World(WithinHelpers) 33 | 34 | # Single-line step scoper 35 | When /^(.*) within (.*[^:])$/ do |step, parent| 36 | with_scope(parent) { When step } 37 | end 38 | 39 | # Multi-line step scoper 40 | When /^(.*) within (.*[^:]):$/ do |step, parent, table_or_string| 41 | with_scope(parent) { When "#{step}:", table_or_string } 42 | end 43 | 44 | Given /^(?:|I )am on (.+)$/ do |page_name| 45 | visit path_to(page_name) 46 | end 47 | 48 | When /^(?:|I )go to (.+)$/ do |page_name| 49 | visit path_to(page_name) 50 | end 51 | 52 | When /^(?:|I )press "([^"]*)"$/ do |button| 53 | click_button(button) 54 | end 55 | 56 | When /^(?:|I )follow "([^"]*)"$/ do |link| 57 | click_link(link) 58 | end 59 | 60 | When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value| 61 | fill_in(field, :with => value) 62 | end 63 | 64 | When /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field| 65 | fill_in(field, :with => value) 66 | end 67 | 68 | # Use this to fill in an entire form with data from a table. Example: 69 | # 70 | # When I fill in the following: 71 | # | Account Number | 5002 | 72 | # | Expiry date | 2009-11-01 | 73 | # | Note | Nice guy | 74 | # | Wants Email? | | 75 | # 76 | # TODO: Add support for checkbox, select og option 77 | # based on naming conventions. 78 | # 79 | When /^(?:|I )fill in the following:$/ do |fields| 80 | fields.rows_hash.each do |name, value| 81 | When %{I fill in "#{name}" with "#{value}"} 82 | end 83 | end 84 | 85 | When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field| 86 | select(value, :from => field) 87 | end 88 | 89 | When /^(?:|I )check "([^"]*)"$/ do |field| 90 | check(field) 91 | end 92 | 93 | When /^(?:|I )uncheck "([^"]*)"$/ do |field| 94 | uncheck(field) 95 | end 96 | 97 | When /^(?:|I )choose "([^"]*)"$/ do |field| 98 | choose(field) 99 | end 100 | 101 | When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field| 102 | attach_file(field, File.expand_path(path)) 103 | end 104 | 105 | Then /^(?:|I )should see "([^"]*)"$/ do |text| 106 | expect(page).to have_content(text) 107 | end 108 | -------------------------------------------------------------------------------- /spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'paperclip/matchers' 3 | 4 | describe Paperclip::Shoulda::Matchers::ValidateAttachmentContentTypeMatcher do 5 | extend Paperclip::Shoulda::Matchers 6 | 7 | before do 8 | reset_table("dummies") do |d| 9 | d.string :title 10 | d.string :avatar_file_name 11 | d.string :avatar_content_type 12 | end 13 | reset_class "Dummy" 14 | Dummy.do_not_validate_attachment_file_type :avatar 15 | Dummy.has_attached_file :avatar 16 | end 17 | 18 | it "rejects a class with no validation" do 19 | expect(matcher).to_not accept(Dummy) 20 | end 21 | 22 | it 'rejects a class when the validation fails' do 23 | Dummy.validates_attachment_content_type :avatar, content_type: %r{audio/.*} 24 | expect(matcher).to_not accept(Dummy) 25 | end 26 | 27 | it "accepts a class with a matching validation" do 28 | Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*} 29 | expect(matcher).to accept(Dummy) 30 | end 31 | 32 | it "accepts a class with other validations but matching types" do 33 | Dummy.validates_presence_of :title 34 | Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*} 35 | expect(matcher).to accept(Dummy) 36 | end 37 | 38 | it "accepts a class that matches and a matcher that only specifies 'allowing'" do 39 | Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*} 40 | matcher = plain_matcher.allowing(%w(image/png image/jpeg)) 41 | 42 | expect(matcher).to accept(Dummy) 43 | end 44 | 45 | it "rejects a class that does not match and a matcher that only specifies 'allowing'" do 46 | Dummy.validates_attachment_content_type :avatar, content_type: %r{audio/.*} 47 | matcher = plain_matcher.allowing(%w(image/png image/jpeg)) 48 | 49 | expect(matcher).to_not accept(Dummy) 50 | end 51 | 52 | it "accepts a class that matches and a matcher that only specifies 'rejecting'" do 53 | Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*} 54 | matcher = plain_matcher.rejecting(%w(audio/mp3 application/octet-stream)) 55 | 56 | expect(matcher).to accept(Dummy) 57 | end 58 | 59 | it "rejects a class that does not match and a matcher that only specifies 'rejecting'" do 60 | Dummy.validates_attachment_content_type :avatar, content_type: %r{audio/.*} 61 | matcher = plain_matcher.rejecting(%w(audio/mp3 application/octet-stream)) 62 | 63 | expect(matcher).to_not accept(Dummy) 64 | end 65 | 66 | context "using an :if to control the validation" do 67 | before do 68 | Dummy.class_eval do 69 | validates_attachment_content_type :avatar, content_type: %r{image/*} , if: :go 70 | attr_accessor :go 71 | end 72 | end 73 | 74 | it "runs the validation if the control is true" do 75 | dummy = Dummy.new 76 | dummy.go = true 77 | expect(matcher).to accept(dummy) 78 | end 79 | 80 | it "does not run the validation if the control is false" do 81 | dummy = Dummy.new 82 | dummy.go = false 83 | expect(matcher).to_not accept(dummy) 84 | end 85 | end 86 | 87 | private 88 | 89 | def plain_matcher 90 | self.class.validate_attachment_content_type(:avatar) 91 | end 92 | 93 | def matcher 94 | plain_matcher. 95 | allowing(%w(image/png image/jpeg)). 96 | rejecting(%w(audio/mp3 application/octet-stream)) 97 | end 98 | 99 | end 100 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/uri_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::UriAdapter do 4 | context "a new instance" do 5 | before do 6 | @open_return = StringIO.new("xxx") 7 | @open_return.stubs(:content_type).returns("image/png") 8 | Paperclip::UriAdapter.any_instance.stubs(:download_content).returns(@open_return) 9 | @uri = URI.parse("http://thoughtbot.com/images/thoughtbot-logo.png") 10 | @subject = Paperclip.io_adapters.for(@uri) 11 | end 12 | 13 | it "returns a file name" do 14 | assert_equal "thoughtbot-logo.png", @subject.original_filename 15 | end 16 | 17 | it 'closes open handle after reading' do 18 | assert_equal true, @open_return.closed? 19 | end 20 | 21 | it "returns a content type" do 22 | assert_equal "image/png", @subject.content_type 23 | end 24 | 25 | it "returns the size of the data" do 26 | assert_equal @open_return.size, @subject.size 27 | end 28 | 29 | it "generates an MD5 hash of the contents" do 30 | assert_equal Digest::MD5.hexdigest("xxx"), @subject.fingerprint 31 | end 32 | 33 | it "generates correct fingerprint after read" do 34 | fingerprint = Digest::MD5.hexdigest(@subject.read) 35 | assert_equal fingerprint, @subject.fingerprint 36 | end 37 | 38 | it "generates same fingerprint" do 39 | assert_equal @subject.fingerprint, @subject.fingerprint 40 | end 41 | 42 | it "returns the data contained in the StringIO" do 43 | assert_equal "xxx", @subject.read 44 | end 45 | 46 | it 'accepts a content_type' do 47 | @subject.content_type = 'image/png' 48 | assert_equal 'image/png', @subject.content_type 49 | end 50 | 51 | it 'accepts an orgiginal_filename' do 52 | @subject.original_filename = 'image.png' 53 | assert_equal 'image.png', @subject.original_filename 54 | end 55 | 56 | end 57 | 58 | context "a directory index url" do 59 | before do 60 | Paperclip::UriAdapter.any_instance.stubs(:download_content).returns(StringIO.new("xxx")) 61 | @uri = URI.parse("http://thoughtbot.com") 62 | @subject = Paperclip.io_adapters.for(@uri) 63 | end 64 | 65 | it "returns a file name" do 66 | assert_equal "index.html", @subject.original_filename 67 | end 68 | 69 | it "returns a content type" do 70 | assert_equal "text/html", @subject.content_type 71 | end 72 | end 73 | 74 | context "a url with query params" do 75 | before do 76 | Paperclip::UriAdapter.any_instance.stubs(:download_content).returns(StringIO.new("xxx")) 77 | @uri = URI.parse("https://github.com/thoughtbot/paperclip?file=test") 78 | @subject = Paperclip.io_adapters.for(@uri) 79 | end 80 | 81 | it "returns a file name" do 82 | assert_equal "paperclip", @subject.original_filename 83 | end 84 | end 85 | 86 | context "a url with restricted characters in the filename" do 87 | before do 88 | Paperclip::UriAdapter.any_instance.stubs(:download_content).returns(StringIO.new("xxx")) 89 | @uri = URI.parse("https://github.com/thoughtbot/paper:clip.jpg") 90 | @subject = Paperclip.io_adapters.for(@uri) 91 | end 92 | 93 | it "does not generate filenames that include restricted characters" do 94 | assert_equal "paper_clip.jpg", @subject.original_filename 95 | end 96 | 97 | it "does not generate paths that include restricted characters" do 98 | expect(@subject.path).to_not match(/:/) 99 | end 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/paperclip/matchers/validate_attachment_content_type_matcher.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Shoulda 3 | module Matchers 4 | # Ensures that the given instance or class validates the content type of 5 | # the given attachment as specified. 6 | # 7 | # Example: 8 | # describe User do 9 | # it { should validate_attachment_content_type(:icon). 10 | # allowing('image/png', 'image/gif'). 11 | # rejecting('text/plain', 'text/xml') } 12 | # end 13 | def validate_attachment_content_type name 14 | ValidateAttachmentContentTypeMatcher.new(name) 15 | end 16 | 17 | class ValidateAttachmentContentTypeMatcher 18 | def initialize attachment_name 19 | @attachment_name = attachment_name 20 | @allowed_types = [] 21 | @rejected_types = [] 22 | end 23 | 24 | def allowing *types 25 | @allowed_types = types.flatten 26 | self 27 | end 28 | 29 | def rejecting *types 30 | @rejected_types = types.flatten 31 | self 32 | end 33 | 34 | def matches? subject 35 | @subject = subject 36 | @subject = @subject.new if @subject.class == Class 37 | @allowed_types && @rejected_types && 38 | allowed_types_allowed? && rejected_types_rejected? 39 | end 40 | 41 | def failure_message 42 | "#{expected_attachment}\n".tap do |message| 43 | message << accepted_types_and_failures 44 | message << "\n\n" if @allowed_types.present? && @rejected_types.present? 45 | message << rejected_types_and_failures 46 | end 47 | end 48 | 49 | def description 50 | "validate the content types allowed on attachment #{@attachment_name}" 51 | end 52 | 53 | protected 54 | 55 | def accepted_types_and_failures 56 | if @allowed_types.present? 57 | "Accept content types: #{@allowed_types.join(", ")}\n".tap do |message| 58 | if @missing_allowed_types.any? 59 | message << " #{@missing_allowed_types.join(", ")} were rejected." 60 | else 61 | message << " All were accepted successfully." 62 | end 63 | end 64 | end 65 | end 66 | def rejected_types_and_failures 67 | if @rejected_types.present? 68 | "Reject content types: #{@rejected_types.join(", ")}\n".tap do |message| 69 | if @missing_rejected_types.any? 70 | message << " #{@missing_rejected_types.join(", ")} were accepted." 71 | else 72 | message << " All were rejected successfully." 73 | end 74 | end 75 | end 76 | end 77 | 78 | def expected_attachment 79 | "Expected #{@attachment_name}:\n" 80 | end 81 | 82 | def type_allowed?(type) 83 | @subject.send("#{@attachment_name}_content_type=", type) 84 | @subject.valid? 85 | @subject.errors[:"#{@attachment_name}_content_type"].blank? 86 | end 87 | 88 | def allowed_types_allowed? 89 | @missing_allowed_types ||= @allowed_types.reject { |type| type_allowed?(type) } 90 | @missing_allowed_types.none? 91 | end 92 | 93 | def rejected_types_rejected? 94 | @missing_rejected_types ||= @rejected_types.select { |type| type_allowed?(type) } 95 | @missing_rejected_types.none? 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /features/basic_integration.feature: -------------------------------------------------------------------------------- 1 | Feature: Rails integration 2 | 3 | Background: 4 | Given I generate a new rails application 5 | And I run a rails generator to generate a "User" scaffold with "name:string" 6 | And I run a paperclip generator to add a paperclip "attachment" to the "User" model 7 | And I run a migration 8 | And I update my new user view to include the file upload field 9 | And I update my user view to include the attachment 10 | And I allow the attachment to be submitted 11 | 12 | Scenario: Configure defaults for all attachments through Railtie 13 | Given I add this snippet to config/application.rb: 14 | """ 15 | config.paperclip_defaults = { 16 | :url => "/paperclip/custom/:attachment/:style/:filename", 17 | :validate_media_type => false 18 | } 19 | """ 20 | And I attach :attachment 21 | And I start the rails application 22 | When I go to the new user page 23 | And I fill in "Name" with "something" 24 | And I attach the file "spec/support/fixtures/animated.unknown" to "Attachment" 25 | And I press "Submit" 26 | Then I should see "Name: something" 27 | And I should see an image with a path of "/paperclip/custom/attachments/original/animated.unknown" 28 | And the file at "/paperclip/custom/attachments/original/animated.unknown" should be the same as "spec/support/fixtures/animated.unknown" 29 | 30 | Scenario: Add custom processors 31 | Given I add a "test" processor in "lib/paperclip" 32 | And I add a "cool" processor in "lib/paperclip_processors" 33 | And I attach :attachment with: 34 | """ 35 | styles: { original: {} }, processors: [:test, :cool] 36 | """ 37 | And I start the rails application 38 | When I go to the new user page 39 | And I fill in "Name" with "something" 40 | And I attach the file "spec/support/fixtures/5k.png" to "Attachment" 41 | And I press "Submit" 42 | Then I should see "Name: something" 43 | And I should see an image with a path of "/paperclip/custom/attachments/original/5k.png" 44 | 45 | Scenario: Filesystem integration test 46 | Given I attach :attachment with: 47 | """ 48 | :url => "/system/:attachment/:style/:filename" 49 | """ 50 | And I start the rails application 51 | When I go to the new user page 52 | And I fill in "Name" with "something" 53 | And I attach the file "spec/support/fixtures/5k.png" to "Attachment" 54 | And I press "Submit" 55 | Then I should see "Name: something" 56 | And I should see an image with a path of "/system/attachments/original/5k.png" 57 | And the file at "/system/attachments/original/5k.png" should be the same as "spec/support/fixtures/5k.png" 58 | 59 | Scenario: S3 Integration test 60 | Given I attach :attachment with: 61 | """ 62 | :storage => :s3, 63 | :path => "/:attachment/:style/:filename", 64 | :s3_credentials => Rails.root.join("config/s3.yml"), 65 | :styles => { :square => "100x100#" } 66 | """ 67 | And I write to "config/s3.yml" with: 68 | """ 69 | bucket: paperclip 70 | access_key_id: access_key 71 | secret_access_key: secret_key 72 | s3_region: us-west-2 73 | """ 74 | And I start the rails application 75 | When I go to the new user page 76 | And I fill in "Name" with "something" 77 | And I attach the file "spec/support/fixtures/5k.png" to "Attachment" on S3 78 | And I press "Submit" 79 | Then I should see "Name: something" 80 | And I should see an image with a path of "http://s3.amazonaws.com/paperclip/attachments/original/5k.png" 81 | And the file at "http://s3.amazonaws.com/paperclip/attachments/original/5k.png" should be uploaded to S3 82 | -------------------------------------------------------------------------------- /spec/paperclip/rake_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rake' 3 | load './lib/tasks/paperclip.rake' 4 | 5 | describe Rake do 6 | context "calling `rake paperclip:refresh:thumbnails`" do 7 | before do 8 | rebuild_model 9 | Paperclip::Task.stubs(:obtain_class).returns('Dummy') 10 | @bogus_instance = Dummy.new 11 | @bogus_instance.id = 'some_id' 12 | @bogus_instance.avatar.stubs(:reprocess!) 13 | @valid_instance = Dummy.new 14 | @valid_instance.avatar.stubs(:reprocess!) 15 | Paperclip::Task.stubs(:log_error) 16 | Paperclip.stubs(:each_instance_with_attachment).multiple_yields @bogus_instance, @valid_instance 17 | end 18 | context "when there is an exception in reprocess!" do 19 | before do 20 | @bogus_instance.avatar.stubs(:reprocess!).raises 21 | end 22 | 23 | it "catches the exception" do 24 | assert_nothing_raised do 25 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 26 | end 27 | end 28 | 29 | it "continues to the next instance" do 30 | @valid_instance.avatar.expects(:reprocess!) 31 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 32 | end 33 | 34 | it "prints the exception" do 35 | exception_msg = 'Some Exception' 36 | @bogus_instance.avatar.stubs(:reprocess!).raises(exception_msg) 37 | Paperclip::Task.expects(:log_error).with do |str| 38 | str.match exception_msg 39 | end 40 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 41 | end 42 | 43 | it "prints the class name" do 44 | Paperclip::Task.expects(:log_error).with do |str| 45 | str.match 'Dummy' 46 | end 47 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 48 | end 49 | 50 | it "prints the instance ID" do 51 | Paperclip::Task.expects(:log_error).with do |str| 52 | str.match "ID #{@bogus_instance.id}" 53 | end 54 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 55 | end 56 | end 57 | 58 | context "when there is an error in reprocess!" do 59 | before do 60 | @errors = mock('errors') 61 | @errors.stubs(:full_messages).returns(['']) 62 | @errors.stubs(:blank?).returns(false) 63 | @bogus_instance.stubs(:errors).returns(@errors) 64 | end 65 | 66 | it "continues to the next instance" do 67 | @valid_instance.avatar.expects(:reprocess!) 68 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 69 | end 70 | 71 | it "prints the error" do 72 | error_msg = 'Some Error' 73 | @errors.stubs(:full_messages).returns([error_msg]) 74 | Paperclip::Task.expects(:log_error).with do |str| 75 | str.match error_msg 76 | end 77 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 78 | end 79 | 80 | it "prints the class name" do 81 | Paperclip::Task.expects(:log_error).with do |str| 82 | str.match 'Dummy' 83 | end 84 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 85 | end 86 | 87 | it "prints the instance ID" do 88 | Paperclip::Task.expects(:log_error).with do |str| 89 | str.match "ID #{@bogus_instance.id}" 90 | end 91 | ::Rake::Task['paperclip:refresh:thumbnails'].execute 92 | end 93 | end 94 | end 95 | 96 | context "Paperclip::Task.log_error method" do 97 | it "prints its argument to STDERR" do 98 | msg = 'Some Message' 99 | $stderr.expects(:puts).with(msg) 100 | Paperclip::Task.log_error(msg) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/paperclip/validators/attachment_content_type_validator.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Validators 3 | class AttachmentContentTypeValidator < ActiveModel::EachValidator 4 | def initialize(options) 5 | options[:allow_nil] = true unless options.has_key?(:allow_nil) 6 | super 7 | end 8 | 9 | def self.helper_method_name 10 | :validates_attachment_content_type 11 | end 12 | 13 | def validate_each(record, attribute, value) 14 | base_attribute = attribute.to_sym 15 | attribute = "#{attribute}_content_type".to_sym 16 | value = record.send :read_attribute_for_validation, attribute 17 | 18 | return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 19 | 20 | validate_whitelist(record, attribute, value) 21 | validate_blacklist(record, attribute, value) 22 | 23 | if record.errors.include? attribute 24 | record.errors[attribute].each do |error| 25 | record.errors.add base_attribute, error 26 | end 27 | end 28 | end 29 | 30 | def validate_whitelist(record, attribute, value) 31 | if allowed_types.present? && allowed_types.none? { |type| type === value } 32 | mark_invalid record, attribute, allowed_types 33 | end 34 | end 35 | 36 | def validate_blacklist(record, attribute, value) 37 | if forbidden_types.present? && forbidden_types.any? { |type| type === value } 38 | mark_invalid record, attribute, forbidden_types 39 | end 40 | end 41 | 42 | def mark_invalid(record, attribute, types) 43 | record.errors.add attribute, :invalid, options.merge(:types => types.join(', ')) 44 | end 45 | 46 | def allowed_types 47 | [options[:content_type]].flatten.compact 48 | end 49 | 50 | def forbidden_types 51 | [options[:not]].flatten.compact 52 | end 53 | 54 | def check_validity! 55 | unless options.has_key?(:content_type) || options.has_key?(:not) 56 | raise ArgumentError, "You must pass in either :content_type or :not to the validator" 57 | end 58 | end 59 | end 60 | 61 | module HelperMethods 62 | # Places ActiveModel validations on the content type of the file 63 | # assigned. The possible options are: 64 | # * +content_type+: Allowed content types. Can be a single content type 65 | # or an array. Each type can be a String or a Regexp. It should be 66 | # noted that Internet Explorer uploads files with content_types that you 67 | # may not expect. For example, JPEG images are given image/pjpeg and 68 | # PNGs are image/x-png, so keep that in mind when determining how you 69 | # match. Allows all by default. 70 | # * +not+: Forbidden content types. 71 | # * +message+: The message to display when the uploaded file has an invalid 72 | # content type. 73 | # * +if+: A lambda or name of an instance method. Validation will only 74 | # be run is this lambda or method returns true. 75 | # * +unless+: Same as +if+ but validates if lambda or method returns false. 76 | # NOTE: If you do not specify an [attachment]_content_type field on your 77 | # model, content_type validation will work _ONLY upon assignment_ and 78 | # re-validation after the instance has been reloaded will always succeed. 79 | # You'll still need to have a virtual attribute (created by +attr_accessor+) 80 | # name +[attachment]_content_type+ to be able to use this validator. 81 | def validates_attachment_content_type(*attr_names) 82 | options = _merge_attributes(attr_names) 83 | validates_with AttachmentContentTypeValidator, options.dup 84 | validate_before_processing AttachmentContentTypeValidator, options.dup 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/paperclip/paperclip_missing_attachment_styles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Missing Attachment Styles' do 4 | before do 5 | Paperclip::AttachmentRegistry.clear 6 | end 7 | 8 | after do 9 | File.unlink(Paperclip.registered_attachments_styles_path) rescue nil 10 | end 11 | 12 | it "enables to get and set path to registered styles file" do 13 | assert_equal ROOT.join('tmp/public/system/paperclip_attachments.yml').to_s, Paperclip.registered_attachments_styles_path 14 | Paperclip.registered_attachments_styles_path = '/tmp/config/paperclip_attachments.yml' 15 | assert_equal '/tmp/config/paperclip_attachments.yml', Paperclip.registered_attachments_styles_path 16 | Paperclip.registered_attachments_styles_path = nil 17 | assert_equal ROOT.join('tmp/public/system/paperclip_attachments.yml').to_s, Paperclip.registered_attachments_styles_path 18 | end 19 | 20 | it "is able to get current attachment styles" do 21 | assert_equal Hash.new, Paperclip.send(:current_attachments_styles) 22 | rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'} 23 | expected_hash = { Dummy: {avatar: [:big, :croppable]}} 24 | assert_equal expected_hash, Paperclip.send(:current_attachments_styles) 25 | end 26 | 27 | it "is able to save current attachment styles for further comparison" do 28 | rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'} 29 | Paperclip.save_current_attachments_styles! 30 | expected_hash = { Dummy: {avatar: [:big, :croppable]}} 31 | assert_equal expected_hash, YAML.load_file(Paperclip.registered_attachments_styles_path) 32 | end 33 | 34 | it "is able to read registered attachment styles from file" do 35 | rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'} 36 | Paperclip.save_current_attachments_styles! 37 | expected_hash = { Dummy: {avatar: [:big, :croppable]}} 38 | assert_equal expected_hash, Paperclip.send(:get_registered_attachments_styles) 39 | end 40 | 41 | it "is able to calculate differences between registered styles and current styles" do 42 | rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'} 43 | Paperclip.save_current_attachments_styles! 44 | rebuild_model styles: {thumb: 'x100', export: 'x400>', croppable: '600x600>', big: '1000x1000>'} 45 | expected_hash = { Dummy: {avatar: [:export, :thumb]} } 46 | assert_equal expected_hash, Paperclip.missing_attachments_styles 47 | 48 | ActiveRecord::Base.connection.create_table :books, force: true 49 | class ::Book < ActiveRecord::Base 50 | has_attached_file :cover, styles: {small: 'x100', large: '1000x1000>'} 51 | has_attached_file :sample, styles: {thumb: 'x100'} 52 | end 53 | 54 | expected_hash = { 55 | Dummy: {avatar: [:export, :thumb]}, 56 | Book: {sample: [:thumb], cover: [:large, :small]} 57 | } 58 | assert_equal expected_hash, Paperclip.missing_attachments_styles 59 | Paperclip.save_current_attachments_styles! 60 | assert_equal Hash.new, Paperclip.missing_attachments_styles 61 | end 62 | 63 | it "is able to calculate differences when a new attachment is added to a model" do 64 | rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'} 65 | Paperclip.save_current_attachments_styles! 66 | 67 | class ::Dummy 68 | has_attached_file :photo, styles: {small: 'x100', large: '1000x1000>'} 69 | end 70 | 71 | expected_hash = { 72 | Dummy: {photo: [:large, :small]} 73 | } 74 | assert_equal expected_hash, Paperclip.missing_attachments_styles 75 | Paperclip.save_current_attachments_styles! 76 | assert_equal Hash.new, Paperclip.missing_attachments_styles 77 | end 78 | 79 | # It's impossible to build styles hash without loading from database whole bunch of records 80 | it "skips lambda-styles" do 81 | rebuild_model styles: lambda{ |attachment| attachment.instance.other == 'a' ? {thumb: "50x50#"} : {large: "400x400"} } 82 | assert_equal Hash.new, Paperclip.send(:current_attachments_styles) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/attachment_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::AttachmentAdapter do 4 | before do 5 | rebuild_model path: "tmp/:class/:attachment/:style/:filename", styles: {thumb: '50x50'} 6 | @attachment = Dummy.new.avatar 7 | end 8 | 9 | context "for an attachment" do 10 | before do 11 | @file = File.new(fixture_file("5k.png")) 12 | @file.binmode 13 | 14 | @attachment.assign(@file) 15 | @attachment.save 16 | @subject = Paperclip.io_adapters.for(@attachment) 17 | end 18 | 19 | after do 20 | @file.close 21 | @subject.close 22 | end 23 | 24 | it "gets the right filename" do 25 | assert_equal "5k.png", @subject.original_filename 26 | end 27 | 28 | it "forces binmode on tempfile" do 29 | assert @subject.instance_variable_get("@tempfile").binmode? 30 | end 31 | 32 | it "gets the content type" do 33 | assert_equal "image/png", @subject.content_type 34 | end 35 | 36 | it "gets the file's size" do 37 | assert_equal 4456, @subject.size 38 | end 39 | 40 | it "returns false for a call to nil?" do 41 | assert ! @subject.nil? 42 | end 43 | 44 | it "generates a MD5 hash of the contents" do 45 | expected = Digest::MD5.file(@file.path).to_s 46 | assert_equal expected, @subject.fingerprint 47 | end 48 | 49 | it "reads the contents of the file" do 50 | expected = @file.read 51 | actual = @subject.read 52 | assert expected.length > 0 53 | assert_equal expected.length, actual.length 54 | assert_equal expected, actual 55 | end 56 | 57 | end 58 | 59 | context "for a file with restricted characters in the name" do 60 | before do 61 | file_contents = IO.read(fixture_file("animated.gif")) 62 | @file = StringIO.new(file_contents) 63 | @file.stubs(:original_filename).returns('image:restricted.gif') 64 | @file.binmode 65 | 66 | @attachment.assign(@file) 67 | @attachment.save 68 | @subject = Paperclip.io_adapters.for(@attachment) 69 | end 70 | 71 | after do 72 | @subject.close 73 | end 74 | 75 | it "does not generate paths that include restricted characters" do 76 | expect(@subject.path).to_not match(/:/) 77 | end 78 | 79 | it "does not generate filenames that include restricted characters" do 80 | assert_equal 'image_restricted.gif', @subject.original_filename 81 | end 82 | end 83 | 84 | context "for a style" do 85 | before do 86 | @file = File.new(fixture_file("5k.png")) 87 | @file.binmode 88 | 89 | @attachment.assign(@file) 90 | 91 | @thumb = Tempfile.new("thumbnail").tap(&:binmode) 92 | FileUtils.cp @attachment.queued_for_write[:thumb].path, @thumb.path 93 | 94 | @attachment.save 95 | @subject = Paperclip.io_adapters.for(@attachment.styles[:thumb]) 96 | end 97 | 98 | after do 99 | @file.close 100 | @thumb.close 101 | @subject.close 102 | end 103 | 104 | it "gets the original filename" do 105 | assert_equal "5k.png", @subject.original_filename 106 | end 107 | 108 | it "forces binmode on tempfile" do 109 | assert @subject.instance_variable_get("@tempfile").binmode? 110 | end 111 | 112 | it "gets the content type" do 113 | assert_equal "image/png", @subject.content_type 114 | end 115 | 116 | it "gets the thumbnail's file size" do 117 | assert_equal @thumb.size, @subject.size 118 | end 119 | 120 | it "returns false for a call to nil?" do 121 | assert ! @subject.nil? 122 | end 123 | 124 | it "generates a MD5 hash of the contents" do 125 | expected = Digest::MD5.file(@thumb.path).to_s 126 | assert_equal expected, @subject.fingerprint 127 | end 128 | 129 | it "reads the contents of the thumbnail" do 130 | @thumb.rewind 131 | expected = @thumb.read 132 | actual = @subject.read 133 | assert expected.length > 0 134 | assert_equal expected.length, actual.length 135 | assert_equal expected, actual 136 | end 137 | 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/paperclip/storage/filesystem.rb: -------------------------------------------------------------------------------- 1 | module Paperclip 2 | module Storage 3 | # The default place to store attachments is in the filesystem. Files on the local 4 | # filesystem can be very easily served by Apache without requiring a hit to your app. 5 | # They also can be processed more easily after they've been saved, as they're just 6 | # normal files. There are two Filesystem-specific options for has_attached_file: 7 | # * +path+: The location of the repository of attachments on disk. This can (and, in 8 | # almost all cases, should) be coordinated with the value of the +url+ option to 9 | # allow files to be saved into a place where Apache can serve them without 10 | # hitting your app. Defaults to 11 | # ":rails_root/public/:attachment/:id/:style/:basename.:extension" 12 | # By default this places the files in the app's public directory which can be served 13 | # directly. If you are using capistrano for deployment, a good idea would be to 14 | # make a symlink to the capistrano-created system directory from inside your app's 15 | # public directory. 16 | # See Paperclip::Attachment#interpolate for more information on variable interpolaton. 17 | # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension" 18 | # * +override_file_permissions+: This allows you to override the file permissions for files 19 | # saved by paperclip. If you set this to an explicit octal value (0755, 0644, etc) then 20 | # that value will be used to set the permissions for an uploaded file. The default is 0666. 21 | # If you set :override_file_permissions to false, the chmod will be skipped. This allows 22 | # you to use paperclip on filesystems that don't understand unix file permissions, and has the 23 | # added benefit of using the storage directories default umask on those that do. 24 | module Filesystem 25 | def self.extended base 26 | end 27 | 28 | def exists?(style_name = default_style) 29 | if original_filename 30 | File.exist?(path(style_name)) 31 | else 32 | false 33 | end 34 | end 35 | 36 | def flush_writes #:nodoc: 37 | @queued_for_write.each do |style_name, file| 38 | FileUtils.mkdir_p(File.dirname(path(style_name))) 39 | begin 40 | FileUtils.mv(file.path, path(style_name)) 41 | rescue SystemCallError 42 | File.open(path(style_name), "wb") do |new_file| 43 | while chunk = file.read(16 * 1024) 44 | new_file.write(chunk) 45 | end 46 | end 47 | end 48 | unless @options[:override_file_permissions] == false 49 | resolved_chmod = (@options[:override_file_permissions] &~ 0111) || (0666 &~ File.umask) 50 | FileUtils.chmod( resolved_chmod, path(style_name) ) 51 | end 52 | file.rewind 53 | end 54 | 55 | after_flush_writes # allows attachment to clean up temp files 56 | 57 | @queued_for_write = {} 58 | end 59 | 60 | def flush_deletes #:nodoc: 61 | @queued_for_delete.each do |path| 62 | begin 63 | log("deleting #{path}") 64 | FileUtils.rm(path) if File.exist?(path) 65 | rescue Errno::ENOENT => e 66 | # ignore file-not-found, let everything else pass 67 | end 68 | begin 69 | while(true) 70 | path = File.dirname(path) 71 | FileUtils.rmdir(path) 72 | break if File.exist?(path) # Ruby 1.9.2 does not raise if the removal failed. 73 | end 74 | rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES 75 | # Stop trying to remove parent directories 76 | rescue SystemCallError => e 77 | log("There was an unexpected error while deleting directories: #{e.class}") 78 | # Ignore it 79 | end 80 | end 81 | @queued_for_delete = [] 82 | end 83 | 84 | def copy_to_local_file(style, local_dest_path) 85 | FileUtils.cp(path(style), local_dest_path) 86 | end 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/paperclip/io_adapters/file_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Paperclip::FileAdapter do 4 | context "a new instance" do 5 | context "with normal file" do 6 | before do 7 | @file = File.new(fixture_file("5k.png")) 8 | @file.binmode 9 | end 10 | 11 | after do 12 | @file.close 13 | @subject.close if @subject 14 | end 15 | 16 | context 'doing normal things' do 17 | before do 18 | @subject = Paperclip.io_adapters.for(@file) 19 | end 20 | 21 | it 'uses the original filename to generate the tempfile' do 22 | assert @subject.path.ends_with?(".png") 23 | end 24 | 25 | it "gets the right filename" do 26 | assert_equal "5k.png", @subject.original_filename 27 | end 28 | 29 | it "forces binmode on tempfile" do 30 | assert @subject.instance_variable_get("@tempfile").binmode? 31 | end 32 | 33 | it "gets the content type" do 34 | assert_equal "image/png", @subject.content_type 35 | end 36 | 37 | it "returns content type as a string" do 38 | expect(@subject.content_type).to be_a String 39 | end 40 | 41 | it "gets the file's size" do 42 | assert_equal 4456, @subject.size 43 | end 44 | 45 | it "returns false for a call to nil?" do 46 | assert ! @subject.nil? 47 | end 48 | 49 | it "generates a MD5 hash of the contents" do 50 | expected = Digest::MD5.file(@file.path).to_s 51 | assert_equal expected, @subject.fingerprint 52 | end 53 | 54 | it "reads the contents of the file" do 55 | expected = @file.read 56 | assert expected.length > 0 57 | assert_equal expected, @subject.read 58 | end 59 | end 60 | 61 | context "file with multiple possible content type" do 62 | before do 63 | MIME::Types.stubs(:type_for).returns([MIME::Type.new('image/x-png'), MIME::Type.new('image/png')]) 64 | @subject = Paperclip.io_adapters.for(@file) 65 | end 66 | 67 | it "prefers officially registered mime type" do 68 | assert_equal "image/png", @subject.content_type 69 | end 70 | 71 | it "returns content type as a string" do 72 | expect(@subject.content_type).to be_a String 73 | end 74 | end 75 | 76 | context "file with content type derived from file contents on *nix" do 77 | before do 78 | MIME::Types.stubs(:type_for).returns([]) 79 | Paperclip.stubs(:run).returns("application/vnd.ms-office\n") 80 | Paperclip::ContentTypeDetector.any_instance 81 | .stubs(:type_from_mime_magic).returns("application/vnd.ms-office") 82 | 83 | @subject = Paperclip.io_adapters.for(@file) 84 | end 85 | 86 | it "returns content type without newline character" do 87 | assert_equal "application/vnd.ms-office", @subject.content_type 88 | end 89 | end 90 | end 91 | 92 | context "filename with restricted characters" do 93 | before do 94 | @file = File.open(fixture_file("animated.gif")) do |file| 95 | StringIO.new(file.read) 96 | end 97 | @file.stubs(:original_filename).returns('image:restricted.gif') 98 | @subject = Paperclip.io_adapters.for(@file) 99 | end 100 | 101 | after do 102 | @file.close 103 | @subject.close 104 | end 105 | 106 | it "does not generate filenames that include restricted characters" do 107 | assert_equal 'image_restricted.gif', @subject.original_filename 108 | end 109 | 110 | it "does not generate paths that include restricted characters" do 111 | expect(@subject.path).to_not match(/:/) 112 | end 113 | end 114 | 115 | context "empty file" do 116 | before do 117 | @file = Tempfile.new("file_adapter_test") 118 | @subject = Paperclip.io_adapters.for(@file) 119 | end 120 | 121 | after do 122 | @file.close 123 | @subject.close 124 | end 125 | 126 | it "provides correct mime-type" do 127 | assert_match %r{.*/x-empty}, @subject.content_type 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /features/step_definitions/attachment_steps.rb: -------------------------------------------------------------------------------- 1 | module AttachmentHelpers 2 | def fixture_path(filename) 3 | File.expand_path("#{PROJECT_ROOT}/spec/support/fixtures/#{filename}") 4 | end 5 | 6 | def attachment_path(filename) 7 | File.expand_path("public/system/attachments/#{filename}") 8 | end 9 | end 10 | World(AttachmentHelpers) 11 | 12 | When /^I modify my attachment definition to:$/ do |definition| 13 | content = cd(".") { File.read("app/models/user.rb") } 14 | name = content[/has_attached_file :\w+/][/:\w+/] 15 | content.gsub!(/has_attached_file.+end/m, <<-FILE) 16 | #{definition} 17 | do_not_validate_attachment_file_type #{name} 18 | end 19 | FILE 20 | 21 | write_file "app/models/user.rb", content 22 | cd(".") { FileUtils.rm_rf ".rbx" } 23 | end 24 | 25 | When /^I upload the fixture "([^"]*)"$/ do |filename| 26 | run_simple %(bundle exec #{runner_command} "User.create!(:attachment => File.open('#{fixture_path(filename)}'))") 27 | end 28 | 29 | Then /^the attachment "([^"]*)" should have a dimension of (\d+x\d+)$/ do |filename, dimension| 30 | cd(".") do 31 | geometry = `identify -format "%wx%h" "#{attachment_path(filename)}"`.strip 32 | expect(geometry).to eq(dimension) 33 | end 34 | end 35 | 36 | Then /^the attachment "([^"]*)" should exist$/ do |filename| 37 | cd(".") do 38 | expect(File.exist?(attachment_path(filename))).to be true 39 | end 40 | end 41 | 42 | When /^I swap the attachment "([^"]*)" with the fixture "([^"]*)"$/ do |attachment_filename, fixture_filename| 43 | cd(".") do 44 | require 'fileutils' 45 | FileUtils.rm_f attachment_path(attachment_filename) 46 | FileUtils.cp fixture_path(fixture_filename), attachment_path(attachment_filename) 47 | end 48 | end 49 | 50 | Then /^the attachment should have the same content type as the fixture "([^"]*)"$/ do |filename| 51 | cd(".") do 52 | begin 53 | # Use mime/types/columnar if available, for reduced memory usage 54 | require "mime/types/columnar" 55 | rescue LoadError 56 | require "mime/types" 57 | end 58 | 59 | attachment_content_type = `bundle exec #{runner_command} "puts User.last.attachment_content_type"`.strip 60 | expected = MIME::Types.type_for(filename).first.content_type 61 | expect(attachment_content_type).to eq(expected) 62 | end 63 | end 64 | 65 | Then /^the attachment should have the same file name as the fixture "([^"]*)"$/ do |filename| 66 | cd(".") do 67 | attachment_file_name = `bundle exec #{runner_command} "puts User.last.attachment_file_name"`.strip 68 | expect(attachment_file_name).to eq(File.name(fixture_path(filename)).to_s) 69 | end 70 | end 71 | 72 | Then /^the attachment should have the same file size as the fixture "([^"]*)"$/ do |filename| 73 | cd(".") do 74 | attachment_file_size = `bundle exec #{runner_command} "puts User.last.attachment_file_size"`.strip 75 | expect(attachment_file_size).to eq(File.size(fixture_path(filename)).to_s) 76 | end 77 | end 78 | 79 | Then /^the attachment file "([^"]*)" should (not )?exist$/ do |filename, not_exist| 80 | cd(".") do 81 | expect(attachment_path(filename)).not_to be_an_existing_file 82 | end 83 | end 84 | 85 | Then /^I should have attachment columns for "([^"]*)"$/ do |attachment_name| 86 | cd(".") do 87 | columns = eval(`bundle exec #{runner_command} "puts User.columns.map{ |column| [column.name, column.type] }.inspect"`.strip) 88 | expect_columns = [ 89 | ["#{attachment_name}_file_name", :string], 90 | ["#{attachment_name}_content_type", :string], 91 | ["#{attachment_name}_file_size", :integer], 92 | ["#{attachment_name}_updated_at", :datetime] 93 | ] 94 | expect(columns).to include(*expect_columns) 95 | end 96 | end 97 | 98 | Then /^I should not have attachment columns for "([^"]*)"$/ do |attachment_name| 99 | cd(".") do 100 | columns = eval(`bundle exec #{runner_command} "puts User.columns.map{ |column| [column.name, column.type] }.inspect"`.strip) 101 | expect_columns = [ 102 | ["#{attachment_name}_file_name", :string], 103 | ["#{attachment_name}_content_type", :string], 104 | ["#{attachment_name}_file_size", :integer], 105 | ["#{attachment_name}_updated_at", :datetime] 106 | ] 107 | 108 | expect(columns).not_to include(*expect_columns) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/paperclip/style.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Paperclip 3 | # The Style class holds the definition of a thumbnail style, applying 4 | # whatever processing is required to normalize the definition and delaying 5 | # the evaluation of block parameters until useful context is available. 6 | 7 | class Style 8 | 9 | attr_reader :name, :attachment, :format 10 | 11 | # Creates a Style object. +name+ is the name of the attachment, 12 | # +definition+ is the style definition from has_attached_file, which 13 | # can be string, array or hash 14 | def initialize name, definition, attachment 15 | @name = name 16 | @attachment = attachment 17 | if definition.is_a? Hash 18 | @geometry = definition.delete(:geometry) 19 | @format = definition.delete(:format) 20 | @processors = definition.delete(:processors) 21 | @convert_options = definition.delete(:convert_options) 22 | @source_file_options = definition.delete(:source_file_options) 23 | @other_args = definition 24 | elsif definition.is_a? String 25 | @geometry = definition 26 | @format = nil 27 | @other_args = {} 28 | else 29 | @geometry, @format = [definition, nil].flatten[0..1] 30 | @other_args = {} 31 | end 32 | @format = default_format if @format.blank? 33 | end 34 | 35 | # retrieves from the attachment the processors defined in the has_attached_file call 36 | # (which method (in the attachment) will call any supplied procs) 37 | # There is an important change of interface here: a style rule can set its own processors 38 | # by default we behave as before, though. 39 | # if a proc has been supplied, we call it here 40 | def processors 41 | @processors.respond_to?(:call) ? @processors.call(attachment.instance) : (@processors || attachment.processors) 42 | end 43 | 44 | # retrieves from the attachment the whiny setting 45 | def whiny 46 | attachment.whiny 47 | end 48 | 49 | # returns true if we're inclined to grumble 50 | def whiny? 51 | !!whiny 52 | end 53 | 54 | def convert_options 55 | @convert_options.respond_to?(:call) ? @convert_options.call(attachment.instance) : 56 | (@convert_options || attachment.send(:extra_options_for, name)) 57 | end 58 | 59 | def source_file_options 60 | @source_file_options.respond_to?(:call) ? @source_file_options.call(attachment.instance) : 61 | (@source_file_options || attachment.send(:extra_source_file_options_for, name)) 62 | end 63 | 64 | # returns the geometry string for this style 65 | # if a proc has been supplied, we call it here 66 | def geometry 67 | @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry 68 | end 69 | 70 | # Supplies the hash of options that processors expect to receive as their second argument 71 | # Arguments other than the standard geometry, format etc are just passed through from 72 | # initialization and any procs are called here, just before post-processing. 73 | def processor_options 74 | args = {:style => name} 75 | @other_args.each do |k,v| 76 | args[k] = v.respond_to?(:call) ? v.call(attachment) : v 77 | end 78 | [:processors, :geometry, :format, :whiny, :convert_options, :source_file_options].each do |k| 79 | (arg = send(k)) && args[k] = arg 80 | end 81 | args 82 | end 83 | 84 | # Supports getting and setting style properties with hash notation to ensure backwards-compatibility 85 | # eg. @attachment.styles[:large][:geometry]@ will still work 86 | def [](key) 87 | if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key) 88 | send(key) 89 | elsif defined? @other_args[key] 90 | @other_args[key] 91 | end 92 | end 93 | 94 | def []=(key, value) 95 | if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key) 96 | send("#{key}=".intern, value) 97 | else 98 | @other_args[key] = value 99 | end 100 | end 101 | 102 | # defaults to default format (nil by default) 103 | def default_format 104 | base = attachment.options[:default_format] 105 | base.respond_to?(:call) ? base.call(attachment, name) : base 106 | end 107 | 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/paperclip/attachment_registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Attachment Registry' do 4 | before do 5 | Paperclip::AttachmentRegistry.clear 6 | end 7 | 8 | context '.names_for' do 9 | it 'includes attachment names for the given class' do 10 | foo = Class.new 11 | Paperclip::AttachmentRegistry.register(foo, :avatar, {}) 12 | 13 | assert_equal [:avatar], Paperclip::AttachmentRegistry.names_for(foo) 14 | end 15 | 16 | it 'does not include attachment names for other classes' do 17 | foo = Class.new 18 | bar = Class.new 19 | Paperclip::AttachmentRegistry.register(foo, :avatar, {}) 20 | Paperclip::AttachmentRegistry.register(bar, :lover, {}) 21 | 22 | assert_equal [:lover], Paperclip::AttachmentRegistry.names_for(bar) 23 | end 24 | 25 | it 'produces the empty array for a missing key' do 26 | assert_empty Paperclip::AttachmentRegistry.names_for(Class.new) 27 | end 28 | end 29 | 30 | context '.each_definition' do 31 | it 'calls the block with the class, attachment name, and options' do 32 | foo = Class.new 33 | expected_accumulations = [ 34 | [foo, :avatar, { yo: "greeting" }], 35 | [foo, :greeter, { ciao: "greeting" }] 36 | ] 37 | expected_accumulations.each do |args| 38 | Paperclip::AttachmentRegistry.register(*args) 39 | end 40 | accumulations = [] 41 | 42 | Paperclip::AttachmentRegistry.each_definition do |*args| 43 | accumulations << args 44 | end 45 | 46 | assert_equal expected_accumulations, accumulations 47 | end 48 | end 49 | 50 | context '.definitions_for' do 51 | it 'produces the attachment name and options' do 52 | expected_definitions = { 53 | avatar: { yo: "greeting" }, 54 | greeter: { ciao: "greeting" } 55 | } 56 | foo = Class.new 57 | Paperclip::AttachmentRegistry.register( 58 | foo, 59 | :avatar, 60 | yo: "greeting" 61 | ) 62 | Paperclip::AttachmentRegistry.register( 63 | foo, 64 | :greeter, 65 | ciao: "greeting" 66 | ) 67 | 68 | definitions = Paperclip::AttachmentRegistry.definitions_for(foo) 69 | 70 | assert_equal expected_definitions, definitions 71 | end 72 | 73 | it 'produces defintions for subclasses' do 74 | expected_definitions = { avatar: { yo: "greeting" } } 75 | foo = Class.new 76 | bar = Class.new(foo) 77 | Paperclip::AttachmentRegistry.register( 78 | foo, 79 | :avatar, 80 | expected_definitions[:avatar] 81 | ) 82 | 83 | definitions = Paperclip::AttachmentRegistry.definitions_for(bar) 84 | 85 | assert_equal expected_definitions, definitions 86 | end 87 | 88 | it 'produces defintions for subclasses but deep merging them' do 89 | foo_definitions = { avatar: { yo: "greeting" } } 90 | bar_definitions = { avatar: { ciao: "greeting" } } 91 | expected_definitions = { 92 | avatar: { 93 | yo: "greeting", 94 | ciao: "greeting" 95 | } 96 | } 97 | foo = Class.new 98 | bar = Class.new(foo) 99 | Paperclip::AttachmentRegistry.register( 100 | foo, 101 | :avatar, 102 | foo_definitions[:avatar] 103 | ) 104 | Paperclip::AttachmentRegistry.register( 105 | bar, 106 | :avatar, 107 | bar_definitions[:avatar] 108 | ) 109 | 110 | definitions = Paperclip::AttachmentRegistry.definitions_for(bar) 111 | 112 | assert_equal expected_definitions, definitions 113 | end 114 | 115 | it 'allows subclasses to override attachment defitions' do 116 | foo_definitions = { avatar: { yo: "greeting" } } 117 | bar_definitions = { avatar: { yo: "hello" } } 118 | 119 | expected_definitions = { 120 | avatar: { 121 | yo: "hello" 122 | } 123 | } 124 | 125 | foo = Class.new 126 | bar = Class.new(foo) 127 | Paperclip::AttachmentRegistry.register( 128 | foo, 129 | :avatar, 130 | foo_definitions[:avatar] 131 | ) 132 | Paperclip::AttachmentRegistry.register( 133 | bar, 134 | :avatar, 135 | bar_definitions[:avatar] 136 | ) 137 | 138 | definitions = Paperclip::AttachmentRegistry.definitions_for(bar) 139 | 140 | assert_equal expected_definitions, definitions 141 | end 142 | end 143 | 144 | context '.clear' do 145 | it 'removes all of the existing attachment definitions' do 146 | foo = Class.new 147 | Paperclip::AttachmentRegistry.register( 148 | foo, 149 | :greeter, 150 | ciao: "greeting" 151 | ) 152 | 153 | Paperclip::AttachmentRegistry.clear 154 | 155 | assert_empty Paperclip::AttachmentRegistry.names_for(foo) 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/paperclip/validators/attachment_size_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validations/numericality' 2 | 3 | module Paperclip 4 | module Validators 5 | class AttachmentSizeValidator < ActiveModel::Validations::NumericalityValidator 6 | AVAILABLE_CHECKS = [:less_than, :less_than_or_equal_to, :greater_than, :greater_than_or_equal_to] 7 | 8 | def initialize(options) 9 | extract_options(options) 10 | super 11 | end 12 | 13 | def self.helper_method_name 14 | :validates_attachment_size 15 | end 16 | 17 | def validate_each(record, attr_name, value) 18 | base_attr_name = attr_name 19 | attr_name = "#{attr_name}_file_size".to_sym 20 | value = record.send(:read_attribute_for_validation, attr_name) 21 | 22 | unless value.blank? 23 | options.slice(*AVAILABLE_CHECKS).each do |option, option_value| 24 | option_value = option_value.call(record) if option_value.is_a?(Proc) 25 | option_value = extract_option_value(option, option_value) 26 | 27 | unless value.send(CHECKS[option], option_value) 28 | error_message_key = options[:in] ? :in_between : option 29 | [ attr_name, base_attr_name ].each do |error_attr_name| 30 | record.errors.add(error_attr_name, error_message_key, filtered_options(value).merge( 31 | :min => min_value_in_human_size(record), 32 | :max => max_value_in_human_size(record), 33 | :count => human_size(option_value) 34 | )) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | 41 | def check_validity! 42 | unless (AVAILABLE_CHECKS + [:in]).any? { |argument| options.has_key?(argument) } 43 | raise ArgumentError, "You must pass either :less_than, :greater_than, or :in to the validator" 44 | end 45 | end 46 | 47 | private 48 | 49 | def extract_options(options) 50 | if range = options[:in] 51 | if !options[:in].respond_to?(:call) 52 | options[:less_than_or_equal_to] = range.max 53 | options[:greater_than_or_equal_to] = range.min 54 | else 55 | options[:less_than_or_equal_to] = range 56 | options[:greater_than_or_equal_to] = range 57 | end 58 | end 59 | end 60 | 61 | def extract_option_value(option, option_value) 62 | if option_value.is_a?(Range) 63 | if [:less_than, :less_than_or_equal_to].include?(option) 64 | option_value.max 65 | else 66 | option_value.min 67 | end 68 | else 69 | option_value 70 | end 71 | end 72 | 73 | def human_size(size) 74 | if defined?(ActiveSupport::NumberHelper) # Rails 4.0+ 75 | ActiveSupport::NumberHelper.number_to_human_size(size) 76 | else 77 | storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) 78 | unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => size.to_i, :raise => true) 79 | storage_units_format.gsub(/%n/, size.to_i.to_s).gsub(/%u/, unit).html_safe 80 | end 81 | end 82 | 83 | def min_value_in_human_size(record) 84 | value = options[:greater_than_or_equal_to] || options[:greater_than] 85 | value = value.call(record) if value.respond_to?(:call) 86 | value = value.min if value.respond_to?(:min) 87 | human_size(value) 88 | end 89 | 90 | def max_value_in_human_size(record) 91 | value = options[:less_than_or_equal_to] || options[:less_than] 92 | value = value.call(record) if value.respond_to?(:call) 93 | value = value.max if value.respond_to?(:max) 94 | human_size(value) 95 | end 96 | end 97 | 98 | module HelperMethods 99 | # Places ActiveModel validations on the size of the file assigned. The 100 | # possible options are: 101 | # * +in+: a Range of bytes (i.e. +1..1.megabyte+), 102 | # * +less_than+: equivalent to :in => 0..options[:less_than] 103 | # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity 104 | # * +message+: error message to display, use :min and :max as replacements 105 | # * +if+: A lambda or name of an instance method. Validation will only 106 | # be run if this lambda or method returns true. 107 | # * +unless+: Same as +if+ but validates if lambda or method returns false. 108 | def validates_attachment_size(*attr_names) 109 | options = _merge_attributes(attr_names) 110 | validates_with AttachmentSizeValidator, options.dup 111 | validate_before_processing AttachmentSizeValidator, options.dup 112 | end 113 | end 114 | end 115 | end 116 | --------------------------------------------------------------------------------