Commit 1655bf29 by Overbryd

Merge branch 'master' into kreuzwerker

parents 4b746b39 94d35f98
...@@ -8,3 +8,6 @@ capybara*.html ...@@ -8,3 +8,6 @@ capybara*.html
*.rbc *.rbc
.bundle .bundle
*SPIKE* *SPIKE*
.rvmrc
*emfile.lock
.rbx
rvm:
- 1.8.7
- 1.9.2
- ree
- rbx-2.0
before_script: "sudo ntpdate -ub ntp.ubuntu.com pool.ntp.org; true"
script: "bundle exec rake clean test cucumber"
gemfile:
- gemfiles/rails2.gemfile
- gemfiles/rails3.gemfile
- gemfiles/rails3_1.gemfile
appraise "rails2" do appraise "rails2" do
gem "rails", "~>2.3.0" gem "rails", "~> 2.3.14"
gem "paperclip", :path => "../"
end end
appraise "rails3" do appraise "rails3" do
gem "rails", "~>3.0.0" gem "rails", "~> 3.0.10"
gem "paperclip", :path => "../"
end end
appraise "rails3_1" do
gem "rails", "~> 3.1.0"
gem "paperclip", :path => "../"
end
We love pull requests. Here's a quick guide:
1. Fork the repo.
2. Run the tests. We only take pull requests with passing tests, and it's great
to know that you have a clean slate: `bundle && rake`
3. Add a test for your change. Only refactoring and documentation changes
require no new tests. If you are adding functionality or fixing a bug, we need
a test!
4. Make the test pass.
5. Push to your fork and submit a pull request.
At this point you're waiting on us. We like to at least comment on, if not
accept, pull requests within three business days (and, typically, one business
day). We may suggest some changes or improvements or alternatives.
Some things that will increase the chance that your pull request is accepted,
taken straight from the Ruby on Rails guide:
* Use Rails idioms and helpers
* Include tests that fail without your code, and pass with it
* Update the documentation, the surrounding one, examples elsewhere, guides,
whatever is affected by your contribution
Syntax:
* Two spaces, no tabs.
* No trailing whitespace. Blank lines should not have any space.
* Prefer &&/|| over and/or.
* MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
* a = b and not a=b.
* Follow the conventions you see used in the source already.
And in case we didn't emphasize it enough: we love tests!
source "http://rubygems.org" source "http://rubygems.org"
gem "shoulda"
gem "activerecord", :require => "active_record"
gem "appraisal"
gem "aruba"
gem "aws-s3", :require => "aws/s3"
gem "bundler"
gem "cocaine", "~>0.2"
gem "fog"
gem "jruby-openssl", :platform => :jruby
gem "mime-types"
gem "mocha" gem "mocha"
gem "rake" gem "rake"
gem "ruby-debug" gem "rdoc", :require => false
gem "aws-s3", :require => "aws/s3" gem "capybara"
gem "sqlite3-ruby", "~>1.3.0" gem "cucumber", "~> 1.0.0"
gem "appraisal" gem "shoulda"
gem "sqlite3", "~>1.3.4"
gem "fakeweb", :require => false
gem 'pry'
GEM
remote: http://rubygems.org/
specs:
appraisal (0.1)
bundler
rake
aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (3.0.0)
columnize (0.3.2)
linecache (0.43)
mime-types (1.16)
mocha (0.9.9)
rake
rake (0.8.7)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
shoulda (2.11.3)
sqlite3-ruby (1.3.2)
xml-simple (1.0.12)
PLATFORMS
ruby
DEPENDENCIES
appraisal
aws-s3
mocha
rake
ruby-debug
shoulda
sqlite3-ruby (~> 1.3.0)
Paperclip # Paperclip [![Build Status](https://secure.travis-ci.org/thoughtbot/paperclip.png?branch=master)](http://travis-ci.org/thoughtbot/paperclip)
=========
Paperclip is intended as an easy file attachment library for ActiveRecord. The Paperclip is intended as an easy file attachment library for ActiveRecord. The
intent behind it was to keep setup as easy as possible and to treat files as intent behind it was to keep setup as easy as possible and to treat files as
...@@ -26,23 +25,47 @@ that it does, on your command line, run `which convert` (one of the ImageMagick ...@@ -26,23 +25,47 @@ that it does, on your command line, run `which convert` (one of the ImageMagick
utilities). This will give you the path where that utility is installed. For utilities). This will give you the path where that utility is installed. For
example, it might return `/usr/local/bin/convert`. example, it might return `/usr/local/bin/convert`.
Then, in your environment config file, let Paperclip know to look there by adding that Then, in your environment config file, let Paperclip know to look there by adding that
directory to its path. directory to its path.
In development mode, you might add this line to `config/environments/development.rb)`: In development mode, you might add this line to `config/environments/development.rb)`:
Paperclip.options[:command_path] = "/usr/local/bin/" Paperclip.options[:command_path] = "/usr/local/bin/"
If you're on Mac OSX, you'll want to run the following with Homebrew:
brew install imagemagick
If you are dealing with pdf uploads or running the test suite, also run:
brew install gs
Installation Installation
------------ ------------
Paperclip is distributed as a gem, which is how it should be used in your app. It's
technically still installable as a plugin, but that's discouraged, as Rails plays
well with gems.
Include the gem in your Gemfile: Include the gem in your Gemfile:
gem "paperclip", "~> 2.3" gem "paperclip", "~> 2.4"
Or, if you don't use Bundler (though you probably should, even in Rails 2), with config.gem
Or as a plugin: # In config/environment.rb
...
Rails::Initializer.run do |config|
...
config.gem "paperclip", :version => "~> 2.4"
...
end
For Non-Rails usage:
ruby script/plugin install git://github.com/thoughtbot/paperclip.git class ModuleName < ActiveRecord::Base
include Paperclip::Glue
...
end
Quick Start Quick Start
----------- -----------
...@@ -73,7 +96,7 @@ In your migrations: ...@@ -73,7 +96,7 @@ In your migrations:
In your edit and new views: In your edit and new views:
<% form_for :user, @user, :url => user_path, :html => { :multipart => true } do |form| %> <%= form_for :user, @user, :url => user_path, :html => { :multipart => true } do |form| %>
<%= form.file_field :avatar %> <%= form.file_field :avatar %>
<% end %> <% end %>
...@@ -89,13 +112,18 @@ In your show view: ...@@ -89,13 +112,18 @@ In your show view:
<%= image_tag @user.avatar.url(:medium) %> <%= image_tag @user.avatar.url(:medium) %>
<%= image_tag @user.avatar.url(:thumb) %> <%= image_tag @user.avatar.url(:thumb) %>
To detach a file, simply set the attribute to `nil`:
@user.avatar = nil
@user.save
Usage Usage
----- -----
The basics of paperclip are quite simple: Declare that your model has an The basics of paperclip are quite simple: Declare that your model has an
attachment with the has_attached_file method, and give it a name. Paperclip attachment with the has_attached_file method, and give it a name. Paperclip
will wrap up up to four attributes (all prefixed with that attachment's name, will wrap up up to four attributes (all prefixed with that attachment's name,
so you can have multiple attachments per model if you wish) and give the a so you can have multiple attachments per model if you wish) and give them a
friendly front end. The attributes are `<attachment>_file_name`, friendly front end. The attributes are `<attachment>_file_name`,
`<attachment>_file_size`, `<attachment>_content_type`, and `<attachment>_updated_at`. `<attachment>_file_size`, `<attachment>_content_type`, and `<attachment>_updated_at`.
Only `<attachment>_file_name` is required for paperclip to operate. More Only `<attachment>_file_name` is required for paperclip to operate. More
...@@ -178,6 +206,12 @@ or more or the processors, and they are expected to ignore it. ...@@ -178,6 +206,12 @@ or more or the processors, and they are expected to ignore it.
_NOTE: Because processors operate by turning the original attachment into the _NOTE: Because processors operate by turning the original attachment into the
styles, no processors will be run if there are no styles defined._ styles, no processors will be run if there are no styles defined._
If you're interested in caching your thumbnail's width, height and size in the
database, take a look at the [paperclip-meta](https://github.com/y8/paperclip-meta) gem.
Also, if you're interested in generating the thumbnail on-the-fly, you might want
to look into the [attachment_on_the_fly](https://github.com/drpentode/Attachment-on-the-Fly) gem.
Events Events
------ ------
...@@ -196,11 +230,157 @@ _NOTE: Post processing will not even *start* if the attachment is not valid ...@@ -196,11 +230,157 @@ _NOTE: Post processing will not even *start* if the attachment is not valid
according to the validations. Your callbacks and processors will *only* be according to the validations. Your callbacks and processors will *only* be
called with valid attachments._ called with valid attachments._
URI Obfuscation
---------------
Paperclip has an interpolation called `:hash` for obfuscating filenames of
publicly-available files.
Example Usage:
has_attached_file :avatar, {
:url => "/system/:hash.:extension",
:hash_secret => "longSecretString"
}
The `:hash` interpolation will be replaced with a unique hash made up of whatever
is specified in `:hash_data`. The default value for `:hash_data` is `":class/:attachment/:id/:style/:updated_at"`.
`:hash_secret` is required, an exception will be raised if `:hash` is used without `:hash_secret` present.
For more on this feature read the author's own explanation. [https://github.com/thoughtbot/paperclip/pull/416](https://github.com/thoughtbot/paperclip/pull/416)
MD5 Checksum / Fingerprint
-------
A MD5 checksum of the original file assigned will be placed in the model if it
has an attribute named fingerprint. Following the user model migration example
above, the migration would look like the following.
class AddAvatarFingerprintColumnToUser < ActiveRecord::Migration
def self.up
add_column :users, :avatar_fingerprint, :string
end
def self.down
remove_column :users, :avatar_fingerprint
end
end
Custom Attachment Processors
-------
Custom attachment processors can be implemented and their only requirement is
to inherit from `Paperclip::Processor` (see `lib/paperclip/processor.rb`).
For example, when `:styles` are specified for an image attachment, the
thumbnail processor (see `lib/paperclip/thumbnail.rb`) is loaded without having
to specify it as a `:processor` parameter to `has_attached_file`. When any
other processor is defined it must be called out in the `:processors`
parameter if it is to be applied to the attachment. The thumbnail processor
uses the imagemagick `convert` command to do the work of resizing image
thumbnails. It would be easy to create a custom processor that watermarks
an image using imagemagick's `composite` command. Following the
implementation pattern of the thumbnail processor would be a way to implement a
watermark processor. All kinds of attachment processors can be created;
a few utility examples would be compression and encryption processors.
Dynamic Configuration
---------------------
Callable objects (lambdas, Procs) can be used in a number of places for dynamic
configuration throughout Paperclip. This strategy exists in a number of
components of the library but is most significant in the possibilities for
allowing custom styles and processors to be applied for specific model
instances, rather than applying defined styles and processors across all
instances.
Dynamic Styles:
Imagine a user model that had different styles based on the role of the user.
Perhaps some users are bosses (e.g. a User model instance responds to #boss?)
and merit a bigger avatar thumbnail than regular users. The configuration to
determine what style parameters are to be used based on the user role might
look as follows where a boss will receive a `300x300` thumbnail otherwise a
`100x100` thumbnail will be created.
class User < ActiveRecord::Base
has_attached_file :avatar, :styles => lambda { |attachment| { :thumb => (attachment.instance.boss? ? "300x300>" : "100x100>") }
end
Dynamic Processors:
Another contrived example is a user model that is aware of which file processors
should be applied to it (beyond the implied `thumbnail` processor invoked when
`:styles` are defined). Perhaps we have a watermark processor available and it is
only used on the avatars of certain models. The configuration for this might be
where the instance is queried for which processors should be applied to it.
Presumably some users might return `[:thumbnail, :watermark]` for its
processors, where a defined `watermark` processor is invoked after the
`thumbnail` processor already defined by Paperclip.
class User < ActiveRecord::Base
has_attached_file :avatar, :processors => lambda { |instance| instance.processors }
attr_accessor :watermark
end
Deploy
------
Paperclip is aware of new attachment styles you have added in previous deploy. The only thing you should do after each deployment is to call
`rake paperclip:refresh:missing_styles`. It will store current attachment styles in `RAILS_ROOT/public/system/paperclip_attachments.yml`
by default. You can change it by:
Paperclip.registered_attachments_styles_path = '/tmp/config/paperclip_attachments.yml'
Here is an example for Capistrano:
namespace :deploy do
desc "build missing paperclip styles"
task :build_missing_paperclip_styles, :roles => :app do
run "cd #{release_path}; RAILS_ENV=production bundle exec rake paperclip:refresh:missing_styles"
end
end
after("deploy:update_code", "deploy:build_missing_paperclip_styles")
Now you don't have to remember to refresh thumbnails in production everytime you add new style.
Unfortunately it does not work with dynamic styles - it just ignores them.
If you already have working app and don't want `rake paperclip:refresh:missing_styles` to refresh old pictures, you need to tell
Paperclip about existing styles. Simply create paperclip_attachments.yml file by hand. For example:
class User < ActiveRecord::Base
has_attached_file :avatar, :styles => {:thumb => 'x100', :croppable => '600x600>', :big => '1000x1000>'}
end
class Book < ActiveRecord::Base
has_attached_file :cover, :styles => {:small => 'x100', :large => '1000x1000>'}
has_attached_file :sample, :styles => {:thumb => 'x100'}
end
Then in `RAILS_ROOT/public/system/paperclip_attachments.yml`:
---
:User:
:avatar:
- :thumb
- :croppable
- :big
:Book:
:cover:
- :small
- :large
:sample:
- :thumb
Testing Testing
------- -------
Paperclip provides rspec-compatible matchers for testing attachments. See the Paperclip provides rspec-compatible matchers for testing attachments. See the
documentation on Paperclip::Shoulda::Matchers for more information. documentation on [Paperclip::Shoulda::Matchers](http://rubydoc.info/gems/paperclip/Paperclip/Shoulda/Matchers)
for more information.
Contributing Contributing
------------ ------------
...@@ -209,12 +389,14 @@ If you'd like to contribute a feature or bugfix: Thanks! To make sure your ...@@ -209,12 +389,14 @@ If you'd like to contribute a feature or bugfix: Thanks! To make sure your
fix/feature has a high chance of being included, please read the following fix/feature has a high chance of being included, please read the following
guidelines: guidelines:
1. Ask on the mailing list[http://groups.google.com/group/paperclip-plugin], or 1. Ask on the mailing list[http://groups.google.com/group/paperclip-plugin], or
post a new GitHub Issue[http://github.com/thoughtbot/paperclip/issues]. post a new GitHub Issue[http://github.com/thoughtbot/paperclip/issues].
2. Make sure there are tests! We will not accept any patch that is not tested. 2. Make sure there are tests! We will not accept any patch that is not tested.
It's a rare time when explicit tests aren't needed. If you have questions It's a rare time when explicit tests aren't needed. If you have questions
about writing tests for paperclip, please ask the mailing list. about writing tests for paperclip, please ask the mailing list.
Please see CONTRIBUTING.md for details.
Credits Credits
------- -------
......
require 'rubygems' require 'rubygems'
require 'appraisal'
require 'bundler/setup' require 'bundler/setup'
require 'appraisal'
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
require 'rake/rdoctask' require 'rdoc/task'
require 'cucumber/rake/task'
$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib') $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
require 'paperclip' require 'paperclip'
desc 'Default: run unit tests.' desc 'Default: run unit tests.'
task :default => [:clean, :all] task :default => [:clean, 'appraisal:install', :all]
desc 'Test the paperclip plugin under all supported Rails versions.' desc 'Test the paperclip plugin under all supported Rails versions.'
task :all do |t| task :all do |t|
exec('rake appraisal test') exec('rake appraisal test cucumber')
end end
desc 'Test the paperclip plugin.' desc 'Test the paperclip plugin.'
...@@ -24,6 +25,11 @@ Rake::TestTask.new(:test) do |t| ...@@ -24,6 +25,11 @@ Rake::TestTask.new(:test) do |t|
t.verbose = true t.verbose = true
end end
desc 'Run integration test'
Cucumber::Rake::Task.new do |t|
t.cucumber_opts = %w{--format progress}
end
desc 'Start an IRB session with all necessary files required.' desc 'Start an IRB session with all necessary files required.'
task :shell do |t| task :shell do |t|
chdir File.dirname(__FILE__) chdir File.dirname(__FILE__)
...@@ -31,7 +37,7 @@ task :shell do |t| ...@@ -31,7 +37,7 @@ task :shell do |t|
end end
desc 'Generate documentation for the paperclip plugin.' desc 'Generate documentation for the paperclip plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc| RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc' rdoc.rdoc_dir = 'doc'
rdoc.title = 'Paperclip' rdoc.title = 'Paperclip'
rdoc.options << '--line-numbers' << '--inline-source' rdoc.options << '--line-numbers' << '--inline-source'
......
Feature: Running paperclip in a Rails app
Scenario: Basic utilization
Given I have a rails application
And I save the following as "app/models/user.rb"
"""
class User < ActiveRecord::Base
has_attached_file :avatar
end
"""
When I visit /users/new
And I fill in "user_name" with "something"
And I attach the file "test/fixtures/5k.png" to "user_avatar"
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "/system/avatars/1/original/5k.png"
And the file at "/system/avatars/1/original/5k.png" is the same as "test/fixtures/5k.png"
Feature: Rails integration
Background:
Given I generate a new rails application
And I run a rails generator to generate a "User" scaffold with "name:string"
And I run a paperclip generator to add a paperclip "attachment" to the "User" model
And I run a migration
And I update my new user view to include the file upload field
And I update my user view to include the attachment
Scenario: Filesystem integration test
Given I add this snippet to the User model:
"""
has_attached_file :attachment
"""
And I start the rails application
When I go to the new user page
And I fill in "Name" with "something"
And I attach the file "test/fixtures/5k.png" to "Attachment"
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "/system/attachments/1/original/5k.png"
And the file at "/system/attachments/1/original/5k.png" should be the same as "test/fixtures/5k.png"
Scenario: S3 Integration test
Given I add this snippet to the User model:
"""
has_attached_file :attachment,
:storage => :s3,
:path => "/:attachment/:id/:style/:filename",
:s3_credentials => Rails.root.join("config/s3.yml")
"""
And I write to "config/s3.yml" with:
"""
bucket: paperclip
access_key_id: access_key
secret_access_key: secret_key
"""
And I start the rails application
When I go to the new user page
And I fill in "Name" with "something"
And I attach the file "test/fixtures/5k.png" to "Attachment" on S3
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "http://s3.amazonaws.com/paperclip/attachments/1/original/5k.png"
And the file at "http://s3.amazonaws.com/paperclip/attachments/1/original/5k.png" should be uploaded to S3
Feature: Rake tasks
Background:
Given I generate a new rails application
And I run a rails generator to generate a "User" scaffold with "name:string"
And I run a paperclip generator to add a paperclip "attachment" to the "User" model
And I run a migration
And I prepare my old Rails application for rake task
And I add this snippet to the User model:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename"
"""
Scenario: Paperclip refresh thumbnails task
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename",
:styles => { :medium => "200x200#" }
"""
And I upload the fixture "5k.png"
Then the attachment "medium/5k.png" should have a dimension of 200x200
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename",
:styles => { :medium => "100x100#" }
"""
When I successfully run `bundle exec rake paperclip:refresh:thumbnails CLASS=User --trace`
Then the attachment "original/5k.png" should exist
And the attachment "medium/5k.png" should have a dimension of 100x100
Scenario: Paperclip refresh metadata task
When I upload the fixture "5k.png"
And I swap the attachment "original/5k.png" with the fixture "12k.png"
And I successfully run `bundle exec rake paperclip:refresh:metadata CLASS=User --trace`
Then the attachment should have the same content type as the fixture "12k.png"
And the attachment should have the same file size as the fixture "12k.png"
Scenario: Paperclip refresh missing styles task
When I upload the fixture "5k.png"
Then the attachment file "original/5k.png" should exist
And the attachment file "medium/5k.png" should not exist
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename",
:styles => { :medium => "200x200#" }
"""
When I successfully run `bundle exec rake paperclip:refresh:missing_styles --trace`
Then the attachment file "original/5k.png" should exist
And the attachment file "medium/5k.png" should exist
Scenario: Paperclip clean task
When I upload the fixture "5k.png"
And I upload the fixture "12k.png"
Then the attachment file "original/5k.png" should exist
And the attachment file "original/12k.png" should exist
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename"
validates_attachment_size :attachment, :less_than => 10.kilobytes
"""
And I successfully run `bundle exec rake paperclip:clean CLASS=User --trace`
Then the attachment file "original/5k.png" should exist
But the attachment file "original/12k.png" should not exist
Feature: Running paperclip in a Rails app using basic S3 support
Scenario: Basic utilization
Given I have a rails application
And I save the following as "app/models/user.rb"
"""
class User < ActiveRecord::Base
has_attached_file :avatar,
:storage => :s3,
:path => "/:attachment/:id/:style/:filename",
:s3_credentials => Rails.root.join("config/s3.yml")
end
"""
And I validate my S3 credentials
And I save the following as "config/s3.yml"
"""
bucket: <%= ENV['PAPERCLIP_TEST_BUCKET'] || 'paperclip' %>
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
"""
When I visit /users/new
And I fill in "user_name" with "something"
And I attach the file "test/fixtures/5k.png" to "user_avatar"
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "http://s3.amazonaws.com/paperclip/avatars/1/original/5k.png"
And the file at "http://s3.amazonaws.com/paperclip/avatars/1/original/5k.png" is the same as "test/fixtures/5k.png"
module AttachmentHelpers
def fixture_path(filename)
File.expand_path("#{PROJECT_ROOT}/test/fixtures/#{filename}")
end
def attachment_path(filename)
File.expand_path("public/system/attachments/#{filename}")
end
end
World(AttachmentHelpers)
When /^I modify my attachment definition to:$/ do |definition|
in_current_dir do
File.open("app/models/user.rb", "w") do |file|
file.write <<-FILE
class User < ActiveRecord::Base
#{definition}
end
FILE
end
end
end
When /^I upload the fixture "([^"]*)"$/ do |filename|
run_simple %(bundle exec #{runner_command} "User.create!(:attachment => File.open('#{fixture_path(filename)}'))")
end
Then /^the attachment "([^"]*)" should have a dimension of (\d+x\d+)$/ do |filename, dimension|
in_current_dir do
geometry = `identify -format "%wx%h" "#{attachment_path(filename)}"`.strip
geometry.should == dimension
end
end
Then /^the attachment "([^"]*)" should exist$/ do |filename|
in_current_dir do
File.exists?(attachment_path(filename)).should be
end
end
When /^I swap the attachment "([^"]*)" with the fixture "([^"]*)"$/ do |attachment_filename, fixture_filename|
in_current_dir do
require 'fileutils'
FileUtils.rm_f attachment_path(attachment_filename)
FileUtils.cp fixture_path(fixture_filename), attachment_path(attachment_filename)
end
end
Then /^the attachment should have the same content type as the fixture "([^"]*)"$/ do |filename|
in_current_dir do
require 'mime/types'
attachment_content_type = `bundle exec #{runner_command} "puts User.last.attachment_content_type"`.strip
attachment_content_type.should == MIME::Types.type_for(filename).first.content_type
end
end
Then /^the attachment should have the same file size as the fixture "([^"]*)"$/ do |filename|
in_current_dir do
attachment_file_size = `bundle exec #{runner_command} "puts User.last.attachment_file_size"`.strip
attachment_file_size.should == File.size(fixture_path(filename)).to_s
end
end
Then /^the attachment file "([^"]*)" should (not )?exist$/ do |filename, not_exist|
in_current_dir do
check_file_presence([attachment_path(filename)], !not_exist)
end
end
...@@ -10,5 +10,6 @@ Then %r{^the file at "([^"]*)" is the same as "([^"]*)"$} do |web_file, path| ...@@ -10,5 +10,6 @@ Then %r{^the file at "([^"]*)" is the same as "([^"]*)"$} do |web_file, path|
visit(web_file) visit(web_file)
page.body page.body
end end
actual.force_encoding("UTF-8") if actual.respond_to?(:force_encoding)
actual.should == expected actual.should == expected
end end
Given "I have a rails application" do Given /^I generate a new rails application$/ do
steps %{ steps %{
Given I generate a rails application When I run `bundle exec #{new_application_command} #{APP_NAME}`
And this plugin is available And I cd to "#{APP_NAME}"
And I have a "users" resource with "name:string"
And I turn off class caching And I turn off class caching
Given I save the following as "app/models/user.rb" And I write to "Gemfile" with:
""" """
class User < ActiveRecord::Base source "http://rubygems.org"
end gem "rails", "#{framework_version}"
""" gem "sqlite3"
And I save the following as "config/s3.yml" gem "capybara"
""" gem "gherkin"
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> gem "aws-s3"
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
bucket: paperclip
"""
And I save the following as "app/views/users/new.html.erb"
"""
<% form_for @user, :html => { :multipart => true } do |f| %>
<%= f.text_field :name %>
<%= f.file_field :avatar %>
<%= submit_tag "Submit" %>
<% end %>
""" """
And I save the following as "app/views/users/show.html.erb" And I configure the application to use "paperclip" from this project
And I reset Bundler environment variable
And I successfully run `bundle install --local`
}
end
Given /^I run a rails generator to generate a "([^"]*)" scaffold with "([^"]*)"$/ do |model_name, attributes|
Given %[I successfully run `bundle exec #{generator_command} scaffold #{model_name} #{attributes}`]
end
Given /^I run a paperclip generator to add a paperclip "([^"]*)" to the "([^"]*)" model$/ do |attachment_name, model_name|
Given %[I successfully run `bundle exec #{generator_command} paperclip #{model_name} #{attachment_name}`]
end
Given /^I run a migration$/ do
Given %[I successfully run `bundle exec rake db:migrate`]
end
Given /^I update my new user view to include the file upload field$/ do
if framework_version?("3")
steps %{
Given I overwrite "app/views/users/new.html.erb" with:
"""
<%= form_for @user, :html => { :multipart => true } do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :attachment %>
<%= f.file_field :attachment %>
<%= submit_tag "Submit" %>
<% end %>
"""
}
else
steps %{
Given I overwrite "app/views/users/new.html.erb" with:
"""
<% form_for @user, :html => { :multipart => true } do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :attachment %>
<%= f.file_field :attachment %>
<%= submit_tag "Submit" %>
<% end %>
"""
}
end
end
Given /^I update my user view to include the attachment$/ do
steps %{
Given I overwrite "app/views/users/show.html.erb" with:
""" """
<p>Name: <%= @user.name %></p> <p>Name: <%= @user.name %></p>
<p>Avatar: <%= image_tag @user.avatar.url %></p> <p>Attachment: <%= image_tag @user.attachment.url %></p>
""" """
And I run "script/generate paperclip user avatar"
And the rails application is prepped and running
} }
end end
Given %r{I generate a rails application} do Given /^I add this snippet to the User model:$/ do |snippet|
FileUtils.rm_rf TEMP_ROOT file_name = "app/models/user.rb"
FileUtils.mkdir_p TEMP_ROOT in_current_dir do
Dir.chdir(TEMP_ROOT) do content = File.read(file_name)
`rails _2.3.8_ #{APP_NAME}` File.open(file_name, 'w') { |f| f << content.sub(/end\Z/, "#{snippet}\nend") }
end end
end end
When %r{I save the following as "([^"]*)"} do |path, string| Given /^I start the rails application$/ do
FileUtils.mkdir_p(File.join(CUC_RAILS_ROOT, File.dirname(path))) in_current_dir do
File.open(File.join(CUC_RAILS_ROOT, path), 'w') { |file| file.write(string) } require "./config/environment"
require "capybara/rails"
end
end
Given /^I reload my application$/ do
Rails::Application.reload!
end end
When %r{I turn off class caching} do When %r{I turn off class caching} do
Dir.chdir(CUC_RAILS_ROOT) do in_current_dir do
file = "config/environments/test.rb" file = "config/environments/test.rb"
config = IO.read(file) config = IO.read(file)
config.gsub!(%r{^\s*config.cache_classes.*$}, config.gsub!(%r{^\s*config.cache_classes.*$},
...@@ -56,35 +99,84 @@ When %r{I turn off class caching} do ...@@ -56,35 +99,84 @@ When %r{I turn off class caching} do
end end
end end
When %r{the rails application is prepped and running$} do Given /^I update my application to use Bundler$/ do
When "I reset the database" if framework_version?("2")
When "the rails application is running" boot_config_template = File.read('features/support/fixtures/boot_config.txt')
preinitializer_template = File.read('features/support/fixtures/preinitializer.txt')
gemfile_template = File.read('features/support/fixtures/gemfile.txt')
in_current_dir do
content = File.read("config/boot.rb").sub(/Rails\.boot!/, boot_config_template)
File.open("config/boot.rb", "w") { |file| file.write(content) }
File.open("config/preinitializer.rb", "w") { |file| file.write(preinitializer_template) }
File.open("Gemfile", "w") { |file| file.write(gemfile_template.sub(/RAILS_VERSION/, framework_version)) }
end
end
end end
When %r{I reset the database} do Given /^I prepare my old Rails application for rake task$/ do
When %{I run "rake db:drop db:create db:migrate"} if framework_version?("2.3")
require 'fileutils'
source = File.expand_path('lib/tasks/paperclip.rake')
destination = in_current_dir { File.expand_path("lib/tasks") }
FileUtils.cp source, destination
append_to "Rakefile", "require 'paperclip'"
end
end end
When %r{the rails application is running} do Then /^the file at "([^"]*)" should be the same as "([^"]*)"$/ do |web_file, path|
Dir.chdir(CUC_RAILS_ROOT) do expected = IO.read(path)
require "config/environment" actual = if web_file.match %r{^https?://}
require "capybara/rails" Net::HTTP.get(URI.parse(web_file))
else
visit(web_file)
page.source
end end
actual.force_encoding("UTF-8") if actual.respond_to?(:force_encoding)
actual.should == expected
end
When /^I configure the application to use "([^\"]+)" from this project$/ do |name|
append_to_gemfile "gem '#{name}', :path => '#{PROJECT_ROOT}'"
steps %{And I run `bundle install --local`}
end end
When %r{this plugin is available} do When /^I configure the application to use "([^\"]+)"$/ do |gem_name|
$LOAD_PATH << "#{PROJECT_ROOT}/lib" append_to_gemfile "gem '#{gem_name}'"
require 'paperclip'
When %{I save the following as "vendor/plugins/paperclip/rails/init.rb"},
IO.read("#{PROJECT_ROOT}/rails/init.rb")
end end
When %r{I run "([^"]*)"} do |command| When /^I append gems from Appraisal Gemfile$/ do
Dir.chdir(CUC_RAILS_ROOT) do File.read(ENV['BUNDLE_GEMFILE']).split(/\n/).each do |line|
`#{command}` if line =~ /^gem "(?!rails|appraisal)/
append_to_gemfile line.strip
end
end end
end end
When %r{I have a "([^"]*)" resource with "([^"]*)"} do |resource, fields| When /^I comment out the gem "([^"]*)" from the Gemfile$/ do |gemname|
When %{I run "script/generate scaffold #{resource} #{fields}"} comment_out_gem_in_gemfile gemname
end end
module FileHelpers
def append_to(path, contents)
in_current_dir do
File.open(path, "a") do |file|
file.puts
file.puts contents
end
end
end
def append_to_gemfile(contents)
append_to('Gemfile', contents)
end
def comment_out_gem_in_gemfile(gemname)
in_current_dir do
gemfile = File.read("Gemfile")
gemfile.sub!(/^(\s*)(gem\s*['"]#{gemname})/, "\\1# \\2")
File.open("Gemfile", 'w'){ |file| file.write(gemfile) }
end
end
end
World(FileHelpers)
Given /I validate my S3 credentials/ do When /^I attach the file "([^"]*)" to "([^"]*)" on S3$/ do |file_path, field|
key = ENV['AWS_ACCESS_KEY_ID'] definition = User.attachment_definitions[field.downcase.to_sym]
secret = ENV['AWS_SECRET_ACCESS_KEY'] path = "http://s3.amazonaws.com/paperclip#{definition[:path]}"
path.gsub!(':filename', File.basename(file_path))
key.should_not be_nil path.gsub!(/:([^\/\.]+)/) do |match|
secret.should_not be_nil "([^\/\.]+)"
end
FakeWeb.register_uri(:put, Regexp.new(path), :body => "OK")
When "I attach the file \"#{file_path}\" to \"#{field}\""
end
assert_credentials(key, secret) Then /^the file at "([^"]*)" should be uploaded to S3$/ do |url|
FakeWeb.registered_uri?(:put, url)
end end
# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. # TL;DR: YOU SHOULD DELETE THIS FILE
# It is recommended to regenerate this file in the future when you upgrade to a #
# newer version of cucumber-rails. Consider adding your own code to a new file # This file was generated by Cucumber-Rails and is only here to get you a head start
# instead of editing this one. Cucumber will automatically load all features/**/*.rb # These step definitions are thin wrappers around the Capybara/Webrat API that lets you
# files. # visit pages, interact with widgets and make assertions about page content.
#
# If you use these step definitions as basis for your features you will quickly end up
# with features that are:
#
# * Hard to maintain
# * Verbose to read
#
# A much better approach is to write your own higher level step definitions, following
# the advice in the following blog posts:
#
# * http://benmabey.com/2008/05/19/imperative-vs-declarative-scenarios-in-user-stories.html
# * http://dannorth.net/2011/01/31/whose-domain-is-it-anyway/
# * http://elabs.se/blog/15-you-re-cuking-it-wrong
#
require 'uri' require 'uri'
require 'cgi' require 'cgi'
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths")) require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors"))
module WithinHelpers module WithinHelpers
def with_scope(locator) def with_scope(locator)
locator ? within(locator) { yield } : yield locator ? within(*selector_for(locator)) { yield } : yield
end end
end end
World(WithinHelpers) World(WithinHelpers)
# Single-line step scoper
When /^(.*) within (.*[^:])$/ do |step, parent|
with_scope(parent) { When step }
end
# Multi-line step scoper
When /^(.*) within (.*[^:]):$/ do |step, parent, table_or_string|
with_scope(parent) { When "#{step}:", table_or_string }
end
Given /^(?:|I )am on (.+)$/ do |page_name| Given /^(?:|I )am on (.+)$/ do |page_name|
visit path_to(page_name) visit path_to(page_name)
end end
...@@ -24,32 +49,20 @@ When /^(?:|I )go to (.+)$/ do |page_name| ...@@ -24,32 +49,20 @@ When /^(?:|I )go to (.+)$/ do |page_name|
visit path_to(page_name) visit path_to(page_name)
end end
When /^(?:|I )visit (\/.+)$/ do |page_path| When /^(?:|I )press "([^"]*)"$/ do |button|
visit page_path click_button(button)
end end
When /^(?:|I )press "([^"]*)"(?: within "([^"]*)")?$/ do |button, selector| When /^(?:|I )follow "([^"]*)"$/ do |link|
with_scope(selector) do click_link(link)
click_button(button)
end
end end
When /^(?:|I )follow "([^"]*)"(?: within "([^"]*)")?$/ do |link, selector| When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value|
with_scope(selector) do fill_in(field, :with => value)
click_link(link)
end
end end
When /^(?:|I )fill in "([^"]*)" with "([^"]*)"(?: within "([^"]*)")?$/ do |field, value, selector| When /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field|
with_scope(selector) do fill_in(field, :with => value)
fill_in(field, :with => value)
end
end
When /^(?:|I )fill in "([^"]*)" for "([^"]*)"(?: within "([^"]*)")?$/ do |value, field, selector|
with_scope(selector) do
fill_in(field, :with => value)
end
end end
# Use this to fill in an entire form with data from a table. Example: # Use this to fill in an entire form with data from a table. Example:
...@@ -63,119 +76,92 @@ end ...@@ -63,119 +76,92 @@ end
# TODO: Add support for checkbox, select og option # TODO: Add support for checkbox, select og option
# based on naming conventions. # based on naming conventions.
# #
When /^(?:|I )fill in the following(?: within "([^"]*)")?:$/ do |selector, fields| When /^(?:|I )fill in the following:$/ do |fields|
with_scope(selector) do fields.rows_hash.each do |name, value|
fields.rows_hash.each do |name, value| When %{I fill in "#{name}" with "#{value}"}
When %{I fill in "#{name}" with "#{value}"}
end
end end
end end
When /^(?:|I )select "([^"]*)" from "([^"]*)"(?: within "([^"]*)")?$/ do |value, field, selector| When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field|
with_scope(selector) do select(value, :from => field)
select(value, :from => field)
end
end end
When /^(?:|I )check "([^"]*)"(?: within "([^"]*)")?$/ do |field, selector| When /^(?:|I )check "([^"]*)"$/ do |field|
with_scope(selector) do check(field)
check(field)
end
end end
When /^(?:|I )uncheck "([^"]*)"(?: within "([^"]*)")?$/ do |field, selector| When /^(?:|I )uncheck "([^"]*)"$/ do |field|
with_scope(selector) do uncheck(field)
uncheck(field)
end
end end
When /^(?:|I )choose "([^"]*)"(?: within "([^"]*)")?$/ do |field, selector| When /^(?:|I )choose "([^"]*)"$/ do |field|
with_scope(selector) do choose(field)
choose(field)
end
end end
When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"(?: within "([^"]*)")?$/ do |path, field, selector| When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field|
with_scope(selector) do attach_file(field, File.expand_path(path))
attach_file(field, path)
end
end
Then /^(?:|I )should see JSON:$/ do |expected_json|
require 'json'
expected = JSON.pretty_generate(JSON.parse(expected_json))
actual = JSON.pretty_generate(JSON.parse(response.body))
expected.should == actual
end end
Then /^(?:|I )should see "([^"]*)"(?: within "([^"]*)")?$/ do |text, selector| Then /^(?:|I )should see "([^"]*)"$/ do |text|
with_scope(selector) do if page.respond_to? :should
if page.respond_to? :should page.should have_content(text)
page.should have_content(text) else
else assert page.has_content?(text)
assert page.has_content?(text)
end
end end
end end
Then /^(?:|I )should see \/([^\/]*)\/(?: within "([^"]*)")?$/ do |regexp, selector| Then /^(?:|I )should see \/([^\/]*)\/$/ do |regexp|
regexp = Regexp.new(regexp) regexp = Regexp.new(regexp)
with_scope(selector) do
if page.respond_to? :should if page.respond_to? :should
page.should have_xpath('//*', :text => regexp) page.should have_xpath('//*', :text => regexp)
else else
assert page.has_xpath?('//*', :text => regexp) assert page.has_xpath?('//*', :text => regexp)
end
end end
end end
Then /^(?:|I )should not see "([^"]*)"(?: within "([^"]*)")?$/ do |text, selector| Then /^(?:|I )should not see "([^"]*)"$/ do |text|
with_scope(selector) do if page.respond_to? :should
if page.respond_to? :should page.should have_no_content(text)
page.should have_no_content(text) else
else assert page.has_no_content?(text)
assert page.has_no_content?(text)
end
end end
end end
Then /^(?:|I )should not see \/([^\/]*)\/(?: within "([^"]*)")?$/ do |regexp, selector| Then /^(?:|I )should not see \/([^\/]*)\/$/ do |regexp|
regexp = Regexp.new(regexp) regexp = Regexp.new(regexp)
with_scope(selector) do
if page.respond_to? :should if page.respond_to? :should
page.should have_no_xpath('//*', :text => regexp) page.should have_no_xpath('//*', :text => regexp)
else else
assert page.has_no_xpath?('//*', :text => regexp) assert page.has_no_xpath?('//*', :text => regexp)
end
end end
end end
Then /^the "([^"]*)" field(?: within "([^"]*)")? should contain "([^"]*)"$/ do |field, selector, value| Then /^the "([^"]*)" field(?: within (.*))? should contain "([^"]*)"$/ do |field, parent, value|
with_scope(selector) do with_scope(parent) do
field = find_field(field) field = find_field(field)
field_value = (field.tag_name == 'textarea') ? field.text : field.value if field.value.respond_to? :should
if field_value.respond_to? :should field.value.should =~ /#{value}/
field_value.should =~ /#{value}/
else else
assert_match(/#{value}/, field_value) assert_match(/#{value}/, field.value)
end end
end end
end end
Then /^the "([^"]*)" field(?: within "([^"]*)")? should not contain "([^"]*)"$/ do |field, selector, value| Then /^the "([^"]*)" field(?: within (.*))? should not contain "([^"]*)"$/ do |field, parent, value|
with_scope(selector) do with_scope(parent) do
field = find_field(field) field = find_field(field)
field_value = (field.tag_name == 'textarea') ? field.text : field.value if field.value.respond_to? :should_not
if field_value.respond_to? :should_not field.value.should_not =~ /#{value}/
field_value.should_not =~ /#{value}/
else else
assert_no_match(/#{value}/, field_value) assert_no_match(/#{value}/, field.value)
end end
end end
end end
Then /^the "([^"]*)" checkbox(?: within "([^"]*)")? should be checked$/ do |label, selector| Then /^the "([^"]*)" checkbox(?: within (.*))? should be checked$/ do |label, parent|
with_scope(selector) do with_scope(parent) do
field_checked = find_field(label)['checked'] field_checked = find_field(label)['checked']
if field_checked.respond_to? :should if field_checked.respond_to? :should
field_checked.should be_true field_checked.should be_true
...@@ -185,8 +171,8 @@ Then /^the "([^"]*)" checkbox(?: within "([^"]*)")? should be checked$/ do |labe ...@@ -185,8 +171,8 @@ Then /^the "([^"]*)" checkbox(?: within "([^"]*)")? should be checked$/ do |labe
end end
end end
Then /^the "([^"]*)" checkbox(?: within "([^"]*)")? should not be checked$/ do |label, selector| Then /^the "([^"]*)" checkbox(?: within (.*))? should not be checked$/ do |label, parent|
with_scope(selector) do with_scope(parent) do
field_checked = find_field(label)['checked'] field_checked = find_field(label)['checked']
if field_checked.respond_to? :should if field_checked.respond_to? :should
field_checked.should be_false field_checked.should be_false
...@@ -218,10 +204,6 @@ Then /^(?:|I )should have the following query string:$/ do |expected_pairs| ...@@ -218,10 +204,6 @@ Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
end end
end end
Then /^I save and open the page$/ do
save_and_open_page
end
Then /^show me the page$/ do Then /^show me the page$/ do
save_and_open_page save_and_open_page
end end
require 'aruba/cucumber'
require 'capybara/cucumber' require 'capybara/cucumber'
require 'test/unit/assertions' require 'test/unit/assertions'
World(Test::Unit::Assertions) World(Test::Unit::Assertions)
Before do
@aruba_timeout_seconds = 120
end
require 'fake_web'
FakeWeb.allow_net_connect = false
class Rails::Boot
def run
load_initializer
Rails::Initializer.class_eval do
def load_gems
@bundler_loaded ||= Bundler.require :default, Rails.env
end
end
Rails::Initializer.run(:set_load_path)
end
end
Rails.boot!
source "http://rubygems.org"
gem "rails", "RAILS_VERSION"
gem "rdoc"
gem "sqlite3"
begin
require "rubygems"
require "bundler"
rescue LoadError
raise "Could not load the bundler gem. Install it with `gem install bundler`."
end
if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24")
raise RuntimeError, "Your bundler version is too old for Rails 2.3." +
"Run `gem install bundler` to upgrade."
end
begin
# Set up load paths for all bundled gems
ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__)
Bundler.setup
rescue Bundler::GemNotFound
raise RuntimeError, "Bundler couldn't find some gems." +
"Did you run `bundle install`?"
end
...@@ -8,17 +8,10 @@ module NavigationHelpers ...@@ -8,17 +8,10 @@ module NavigationHelpers
def path_to(page_name) def path_to(page_name)
case page_name case page_name
when /the new user page/
'/users/new'
when /the home\s?page/ when /the home\s?page/
'/' '/'
when /the new user page/
# Add more mappings here. '/users/new'
# Here is an example that pulls values out of the Regexp:
#
# when /^(.*)'s profile page$/i
# user_profile_path(User.find_by_login($1))
else else
begin begin
page_name =~ /the (.*) page/ page_name =~ /the (.*) page/
......
PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze
TEMP_ROOT = File.join(PROJECT_ROOT, 'tmp').freeze
APP_NAME = 'testapp'.freeze APP_NAME = 'testapp'.freeze
CUC_RAILS_ROOT = File.join(TEMP_ROOT, APP_NAME).freeze BUNDLE_ENV_VARS = %w(RUBYOPT BUNDLE_PATH BUNDLE_BIN_PATH BUNDLE_GEMFILE)
ORIGINAL_BUNDLE_VARS = Hash[ENV.select{ |key,value| BUNDLE_ENV_VARS.include?(key) }]
ENV['RAILS_ENV'] = 'test' ENV['RAILS_ENV'] = 'test'
Before do
ENV['BUNDLE_GEMFILE'] = File.join(Dir.pwd, ENV['BUNDLE_GEMFILE']) unless ENV['BUNDLE_GEMFILE'].start_with?(Dir.pwd)
@framework_version = nil
end
After do
ORIGINAL_BUNDLE_VARS.each_pair do |key, value|
ENV[key] = value
end
end
When /^I reset Bundler environment variable$/ do
BUNDLE_ENV_VARS.each do |key|
ENV[key] = nil
end
end
module RailsCommandHelpers
def framework_version?(version_string)
framework_version =~ /^#{version_string}/
end
def framework_version
@framework_version ||= `rails -v`[/^Rails (.+)$/, 1]
end
def new_application_command
framework_version?("3") ? "rails new" : "rails"
end
def generator_command
framework_version?("3") ? "script/rails generate" : "script/generate"
end
def runner_command
framework_version?("3") ? "script/rails runner" : "script/runner"
end
end
World(RailsCommandHelpers)
module AWSS3Methods
def load_s3
begin
require 'aws/s3'
rescue LoadError => e
fail "You do not have aws-s3 installed."
end
end
def assert_credentials(key, secret)
load_s3
begin
AWS::S3::Base.establish_connection!(
:access_key_id => key,
:secret_access_key => secret
)
AWS::S3::Service.buckets
rescue AWS::S3::ResponseError => e
fail "Could not connect using AWS credentials in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. " +
"Please make sure these are set in your environment."
end
end
end
World(AWSS3Methods)
module HtmlSelectorsHelpers
# Maps a name to a selector. Used primarily by the
#
# When /^(.+) within (.+)$/ do |step, scope|
#
# step definitions in web_steps.rb
#
def selector_for(locator)
case locator
when "the page"
"html > body"
else
raise "Can't find mapping from \"#{locator}\" to a selector.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
World(HtmlSelectorsHelpers)
# This file was generated by Appraisal # This file was generated by Appraisal
source "http://rubygems.org" source "http://rubygems.org"
gem "ruby-debug"
gem "rails", "~>2.3.0" gem "activerecord", :require=>"active_record"
gem "appraisal"
gem "aruba"
gem "aws-s3", :require=>"aws/s3"
gem "bundler"
gem "cocaine", "~>0.2"
gem "fog"
gem "jruby-openssl", :platform=>:jruby
gem "mime-types"
gem "mocha"
gem "rake" gem "rake"
gem "sqlite3-ruby", "~>1.3.0" gem "rdoc", :require=>false
gem "capybara"
gem "cucumber", "~> 1.0.0"
gem "shoulda" gem "shoulda"
gem "mocha" gem "sqlite3", "~>1.3.4"
gem "aws-s3", {:require=>"aws/s3"} gem "fakeweb", :require=>false
gem "appraisal" gem "pry"
\ No newline at end of file gem "rails", "~> 2.3.14"
gem "paperclip", :path=>"../"
GEM
remote: http://rubygems.org/
specs:
actionmailer (2.3.10)
actionpack (= 2.3.10)
actionpack (2.3.10)
activesupport (= 2.3.10)
rack (~> 1.1.0)
activerecord (2.3.10)
activesupport (= 2.3.10)
activeresource (2.3.10)
activesupport (= 2.3.10)
activesupport (2.3.10)
appraisal (0.1)
bundler
rake
aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (3.0.0)
columnize (0.3.2)
linecache (0.43)
mime-types (1.16)
mocha (0.9.9)
rake
rack (1.1.0)
rails (2.3.10)
actionmailer (= 2.3.10)
actionpack (= 2.3.10)
activerecord (= 2.3.10)
activeresource (= 2.3.10)
activesupport (= 2.3.10)
rake (>= 0.8.3)
rake (0.8.7)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
shoulda (2.11.3)
sqlite3-ruby (1.3.2)
xml-simple (1.0.12)
PLATFORMS
ruby
DEPENDENCIES
appraisal
aws-s3
mocha
rails (~> 2.3.0)
rake
ruby-debug
shoulda
sqlite3-ruby (~> 1.3.0)
# This file was generated by Appraisal # This file was generated by Appraisal
source "http://rubygems.org" source "http://rubygems.org"
gem "ruby-debug"
gem "rails", "~>3.0.0" gem "activerecord", :require=>"active_record"
gem "appraisal"
gem "aruba"
gem "aws-s3", :require=>"aws/s3"
gem "bundler"
gem "cocaine", "~>0.2"
gem "fog"
gem "jruby-openssl", :platform=>:jruby
gem "mime-types"
gem "mocha"
gem "rake" gem "rake"
gem "sqlite3-ruby", "~>1.3.0" gem "rdoc", :require=>false
gem "capybara"
gem "cucumber", "~> 1.0.0"
gem "shoulda" gem "shoulda"
gem "mocha" gem "sqlite3", "~>1.3.4"
gem "aws-s3", {:require=>"aws/s3"} gem "fakeweb", :require=>false
gem "appraisal" gem "pry"
\ No newline at end of file gem "rails", "~> 3.0.10"
gem "paperclip", :path=>"../"
GEM
remote: http://rubygems.org/
specs:
abstract (1.0.0)
actionmailer (3.0.3)
actionpack (= 3.0.3)
mail (~> 2.2.9)
actionpack (3.0.3)
activemodel (= 3.0.3)
activesupport (= 3.0.3)
builder (~> 2.1.2)
erubis (~> 2.6.6)
i18n (~> 0.4)
rack (~> 1.2.1)
rack-mount (~> 0.6.13)
rack-test (~> 0.5.6)
tzinfo (~> 0.3.23)
activemodel (3.0.3)
activesupport (= 3.0.3)
builder (~> 2.1.2)
i18n (~> 0.4)
activerecord (3.0.3)
activemodel (= 3.0.3)
activesupport (= 3.0.3)
arel (~> 2.0.2)
tzinfo (~> 0.3.23)
activeresource (3.0.3)
activemodel (= 3.0.3)
activesupport (= 3.0.3)
activesupport (3.0.3)
appraisal (0.1)
bundler
rake
arel (2.0.4)
aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (2.1.2)
columnize (0.3.2)
erubis (2.6.6)
abstract (>= 1.0.0)
i18n (0.4.2)
linecache (0.43)
mail (2.2.10)
activesupport (>= 2.3.6)
i18n (~> 0.4.1)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.16)
mocha (0.9.9)
rake
polyglot (0.3.1)
rack (1.2.1)
rack-mount (0.6.13)
rack (>= 1.0.0)
rack-test (0.5.6)
rack (>= 1.0)
rails (3.0.3)
actionmailer (= 3.0.3)
actionpack (= 3.0.3)
activerecord (= 3.0.3)
activeresource (= 3.0.3)
activesupport (= 3.0.3)
bundler (~> 1.0)
railties (= 3.0.3)
railties (3.0.3)
actionpack (= 3.0.3)
activesupport (= 3.0.3)
rake (>= 0.8.7)
thor (~> 0.14.4)
rake (0.8.7)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
shoulda (2.11.3)
sqlite3-ruby (1.3.2)
thor (0.14.6)
treetop (1.4.9)
polyglot (>= 0.3.1)
tzinfo (0.3.23)
xml-simple (1.0.12)
PLATFORMS
ruby
DEPENDENCIES
appraisal
aws-s3
mocha
rails (~> 3.0.0)
rake
ruby-debug
shoulda
sqlite3-ruby (~> 1.3.0)
# This file was generated by Appraisal
source "http://rubygems.org"
gem "activerecord", :require=>"active_record"
gem "appraisal"
gem "aruba"
gem "aws-s3", :require=>"aws/s3"
gem "bundler"
gem "cocaine", "~>0.2"
gem "fog"
gem "jruby-openssl", :platform=>:jruby
gem "mime-types"
gem "mocha"
gem "rake"
gem "rdoc", :require=>false
gem "capybara"
gem "cucumber", "~> 1.0.0"
gem "shoulda"
gem "sqlite3", "~>1.3.4"
gem "fakeweb", :require=>false
gem "pry"
gem "rails", "~> 3.1.0"
gem "paperclip", :path=>"../"
require File.join(File.dirname(__FILE__), "lib", "paperclip") require File.join(File.dirname(__FILE__), "lib", "paperclip")
require 'paperclip/railtie'
Paperclip::Railtie.insert
require 'rails/generators/active_record' require 'rails/generators/active_record'
class PaperclipGenerator < ActiveRecord::Generators::Base class PaperclipGenerator < ActiveRecord::Generators::Base
desc "Create a migration to add paperclip-specific fields to your model." desc "Create a migration to add paperclip-specific fields to your model. " +
"The NAME argument is the name of your model, and the following " +
"arguments are the name of the attachments"
argument :attachment_names, :required => true, :type => :array, :desc => "The names of the attachment(s) to add.", argument :attachment_names, :required => true, :type => :array, :desc => "The names of the attachment(s) to add.",
:banner => "attachment_one attachment_two attachment_three ..." :banner => "attachment_one attachment_two attachment_three ..."
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
# columns to your table. # columns to your table.
# #
# Author:: Jon Yurek # Author:: Jon Yurek
# Copyright:: Copyright (c) 2008-2009 thoughtbot, inc. # Copyright:: Copyright (c) 2008-2011 thoughtbot, inc.
# License:: MIT License (http://www.opensource.org/licenses/mit-license.php) # License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
# #
# Paperclip defines an attachment as any file, though it makes special considerations # Paperclip defines an attachment as any file, though it makes special considerations
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
require 'erb' require 'erb'
require 'digest' require 'digest'
require 'tempfile' require 'tempfile'
require 'paperclip/options'
require 'paperclip/version' require 'paperclip/version'
require 'paperclip/upfile' require 'paperclip/upfile'
require 'paperclip/iostream' require 'paperclip/iostream'
...@@ -38,14 +39,11 @@ require 'paperclip/interpolations' ...@@ -38,14 +39,11 @@ require 'paperclip/interpolations'
require 'paperclip/style' require 'paperclip/style'
require 'paperclip/attachment' require 'paperclip/attachment'
require 'paperclip/storage' require 'paperclip/storage'
require 'paperclip/callback_compatability' require 'paperclip/callback_compatibility'
require 'paperclip/command_line' require 'paperclip/missing_attachment_styles'
require 'paperclip/railtie' require 'paperclip/railtie'
if defined?(Rails.root) && Rails.root require 'logger'
Dir.glob(File.join(File.expand_path(Rails.root), "lib", "paperclip_processors", "*.rb")).each do |processor| require 'cocaine'
require processor
end
end
# The base module that gets included in ActiveRecord::Base. See the # The base module that gets included in ActiveRecord::Base. See the
# documentation for Paperclip::ClassMethods for more useful information. # documentation for Paperclip::ClassMethods for more useful information.
...@@ -80,71 +78,133 @@ module Paperclip ...@@ -80,71 +78,133 @@ module Paperclip
Paperclip::Interpolations[key] = block Paperclip::Interpolations[key] = block
end end
# The run method takes a command to execute and an array of parameters # The run method takes the name of a binary to run, the arguments to that binary
# that get passed to it. The command is prefixed with the :command_path # and some options:
# option from Paperclip.options. If you have many commands to run and #
# they are in different paths, the suggested course of action is to # :command_path -> A $PATH-like variable that defines where to look for the binary
# symlink them so they are all in the same directory. # on the filesystem. Colon-separated, just like $PATH.
# #
# If the command returns with a result code that is not one of the # :expected_outcodes -> An array of integers that defines the expected exit codes
# expected_outcodes, a PaperclipCommandLineError will be raised. Generally # of the binary. Defaults to [0].
# a code of 0 is expected, but a list of codes may be passed if necessary.
# These codes should be passed as a hash as the last argument, like so:
# #
# Paperclip.run("echo", "something", :expected_outcodes => [0,1,2,3]) # :log_command -> Log the command being run when set to true (defaults to false).
# This will only log if logging in general is set to true as well.
# #
# This method can log the command being run when # :swallow_stderr -> Set to true if you don't care what happens on STDERR.
# Paperclip.options[:log_command] is set to true (defaults to false). This #
# will only log if logging in general is set to true as well. def run(cmd, arguments = "", local_options = {})
def run cmd, *params
if options[:image_magick_path] if options[:image_magick_path]
Paperclip.log("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead") Paperclip.log("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
end end
CommandLine.path = options[:command_path] || options[:image_magick_path] command_path = options[:command_path] || options[:image_magick_path]
CommandLine.new(cmd, *params).run Cocaine::CommandLine.path = ( Cocaine::CommandLine.path ? [Cocaine::CommandLine.path, command_path ].flatten : command_path )
local_options = local_options.merge(:logger => logger) if logging? && (options[:log_command] || local_options[:log_command])
Cocaine::CommandLine.new(cmd, arguments, local_options).run
end
def processor(name) #:nodoc:
@known_processors ||= {}
if @known_processors[name.to_s]
@known_processors[name.to_s]
else
name = name.to_s.camelize
load_processor(name) unless Paperclip.const_defined?(name)
processor = Paperclip.const_get(name)
@known_processors[name.to_s] = processor
end
end end
def processor name #:nodoc: def load_processor(name)
name = name.to_s.camelize if defined?(Rails.root) && Rails.root
processor = Paperclip.const_get(name) require File.expand_path(Rails.root.join("lib", "paperclip_processors", "#{name.underscore}.rb"))
unless processor.ancestors.include?(Paperclip::Processor)
raise PaperclipError.new("Processor #{name} was not found")
end end
processor
end end
def clear_processors!
@known_processors.try(:clear)
end
# You can add your own processor via the Paperclip configuration. Normally
# Paperclip will load all processors from the
# Rails.root/lib/paperclip_processors directory, but here you can add any
# existing class using this mechanism.
#
# Paperclip.configure do |c|
# c.register_processor :watermarker, WatermarkingProcessor.new
# end
def register_processor(name, processor)
@known_processors ||= {}
@known_processors[name.to_s] = processor
end
# Find all instances of the given Active Record model +klass+ with attachment +name+.
# This method is used by the refresh rake tasks.
def each_instance_with_attachment(klass, name) def each_instance_with_attachment(klass, name)
Object.const_get(klass).all.each do |instance| class_for(klass).find(:all, :order => 'id').each do |instance|
yield(instance) if instance.send(:"#{name}?") yield(instance) if instance.send(:"#{name}?")
end end
end end
# Log a paperclip-specific line. Uses ActiveRecord::Base.logger # Log a paperclip-specific line. This will logs to STDOUT
# by default. Set Paperclip.options[:log] to false to turn off. # by default. Set Paperclip.options[:log] to false to turn off.
def log message def log message
logger.info("[paperclip] #{message}") if logging? logger.info("[paperclip] #{message}") if logging?
end end
def logger #:nodoc: def logger #:nodoc:
ActiveRecord::Base.logger @logger ||= options[:logger] || Logger.new(STDOUT)
end
def logger=(logger)
@logger = logger
end end
def logging? #:nodoc: def logging? #:nodoc:
options[:log] options[:log]
end end
end
class PaperclipError < StandardError #:nodoc: def class_for(class_name)
end # Ruby 1.9 introduces an inherit argument for Module#const_get and
# #const_defined? and changes their default behavior.
# https://github.com/rails/rails/blob/v3.0.9/activesupport/lib/active_support/inflector/methods.rb#L89
if Module.method(:const_get).arity == 1
class_name.split('::').inject(Object) do |klass, partial_class_name|
klass.const_defined?(partial_class_name) ? klass.const_get(partial_class_name) : klass.const_missing(partial_class_name)
end
else
class_name.split('::').inject(Object) do |klass, partial_class_name|
klass.const_defined?(partial_class_name) ? klass.const_get(partial_class_name, false) : klass.const_missing(partial_class_name)
end
end
rescue ArgumentError => e
# Sadly, we need to capture ArguementError here because Rails 2.3.x
# Active Support dependency's management will try to the constant inherited
# from Object, and fail misably with "Object is not missing constant X" error
# https://github.com/rails/rails/blob/v2.3.12/activesupport/lib/active_support/dependencies.rb#L124
if e.message =~ /is not missing constant/
raise NameError, "uninitialized constant #{class_name}"
else
raise e
end
end
def check_for_url_clash(name,url,klass)
@names_url ||= {}
default_url = url || Attachment.default_options[:url]
if @names_url[name] && @names_url[name][:url] == default_url && @names_url[name][:class] != klass
log("Duplicate URL for #{name} with #{default_url}. This will clash with attachment defined in #{@names_url[name][:class]} class")
end
@names_url[name] = {:url => default_url, :class => klass}
end
class PaperclipCommandLineError < PaperclipError #:nodoc: def reset_duplicate_clash_check!
attr_accessor :output @names_url = nil
def initialize(msg = nil, output = nil)
super(msg)
@output = output
end end
end end
class PaperclipError < StandardError #:nodoc:
end
class StorageMethodNotFound < PaperclipError class StorageMethodNotFound < PaperclipError
end end
...@@ -160,7 +220,8 @@ module Paperclip ...@@ -160,7 +220,8 @@ module Paperclip
module Glue module Glue
def self.included base #:nodoc: def self.included base #:nodoc:
base.extend ClassMethods base.extend ClassMethods
if base.respond_to?("set_callback") base.class_attribute :attachment_definitions if base.respond_to?(:class_attribute)
if base.respond_to?(:set_callback)
base.send :include, Paperclip::CallbackCompatability::Rails3 base.send :include, Paperclip::CallbackCompatability::Rails3
else else
base.send :include, Paperclip::CallbackCompatability::Rails21 base.send :include, Paperclip::CallbackCompatability::Rails21
...@@ -224,7 +285,7 @@ module Paperclip ...@@ -224,7 +285,7 @@ module Paperclip
# } # }
# NOTE: While not deprecated yet, it is not recommended to specify options this way. # NOTE: While not deprecated yet, it is not recommended to specify options this way.
# It is recommended that :convert_options option be included in the hash passed to each # It is recommended that :convert_options option be included in the hash passed to each
# :styles for compatability with future versions. # :styles for compatibility with future versions.
# NOTE: Strings supplied to :convert_options are split on space in order to undergo # NOTE: Strings supplied to :convert_options are split on space in order to undergo
# shell quoting for safety. If your options require a space, please pre-split them # shell quoting for safety. If your options require a space, please pre-split them
# and pass an array to :convert_options instead. # and pass an array to :convert_options instead.
...@@ -232,14 +293,38 @@ module Paperclip ...@@ -232,14 +293,38 @@ module Paperclip
# choices are :filesystem and :s3. The default is :filesystem. Make sure you read the # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
# documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3 # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
# for backend-specific options. # for backend-specific options.
#
# It's also possible for you to dynamicly define your interpolation string for :url,
# :default_url, and :path in your model by passing a method name as a symbol as a argument
# for your has_attached_file definition:
#
# class Person
# has_attached_file :avatar, :default_url => :default_url_by_gender
#
# private
#
# def default_url_by_gender
# "/assets/avatars/default_#{gender}.png"
# end
# end
def has_attached_file name, options = {} def has_attached_file name, options = {}
include InstanceMethods include InstanceMethods
write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil? if attachment_definitions.nil?
if respond_to?(:class_attribute)
self.attachment_definitions = {}
else
write_inheritable_attribute(:attachment_definitions, {})
end
end
attachment_definitions[name] = {:validations => []}.merge(options) attachment_definitions[name] = {:validations => []}.merge(options)
Paperclip.classes_with_attachments << self.name
Paperclip.check_for_url_clash(name,attachment_definitions[name][:url],self.name)
after_save :save_attached_files after_save :save_attached_files
before_destroy :destroy_attached_files before_destroy :prepare_for_destroy
after_destroy :destroy_attached_files
define_paperclip_callbacks :post_process, :"#{name}_post_process" define_paperclip_callbacks :post_process, :"#{name}_post_process"
...@@ -275,7 +360,7 @@ module Paperclip ...@@ -275,7 +360,7 @@ module Paperclip
min = options[:greater_than] || (options[:in] && options[:in].first) || 0 min = options[:greater_than] || (options[:in] && options[:in].first) || 0
max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0) max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
range = (min..max) range = (min..max)
message = options[:message] || "file size must be between :min and :max bytes." message = options[:message] || "file size must be between :min and :max bytes"
message = message.call if message.respond_to?(:call) message = message.call if message.respond_to?(:call)
message = message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s) message = message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
...@@ -301,7 +386,7 @@ module Paperclip ...@@ -301,7 +386,7 @@ module Paperclip
# be run is this lambda or method returns true. # be run is this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false. # * +unless+: Same as +if+ but validates if lambda or method returns false.
def validates_attachment_presence name, options = {} def validates_attachment_presence name, options = {}
message = options[:message] || "must be set." message = options[:message] || :empty
validates_presence_of :"#{name}_file_name", validates_presence_of :"#{name}_file_name",
:message => message, :message => message,
:if => options[:if], :if => options[:if],
...@@ -324,6 +409,8 @@ module Paperclip ...@@ -324,6 +409,8 @@ module Paperclip
# NOTE: If you do not specify an [attachment]_content_type field on your # NOTE: If you do not specify an [attachment]_content_type field on your
# model, content_type validation will work _ONLY upon assignment_ and # model, content_type validation will work _ONLY upon assignment_ and
# re-validation after the instance has been reloaded will always succeed. # re-validation after the instance has been reloaded will always succeed.
# You'll still need to have a virtual attribute (created by +attr_accessor+)
# name +[attachment]_content_type+ to be able to use this validator.
def validates_attachment_content_type name, options = {} def validates_attachment_content_type name, options = {}
validation_options = options.dup validation_options = options.dup
allowed_types = [validation_options[:content_type]].flatten allowed_types = [validation_options[:content_type]].flatten
...@@ -343,7 +430,11 @@ module Paperclip ...@@ -343,7 +430,11 @@ module Paperclip
# Returns the attachment definitions defined by each call to # Returns the attachment definitions defined by each call to
# has_attached_file. # has_attached_file.
def attachment_definitions def attachment_definitions
read_inheritable_attribute(:attachment_definitions) if respond_to?(:class_attribute)
self.attachment_definitions
else
read_inheritable_attribute(:attachment_definitions)
end
end end
end end
...@@ -369,10 +460,17 @@ module Paperclip ...@@ -369,10 +460,17 @@ module Paperclip
def destroy_attached_files def destroy_attached_files
Paperclip.log("Deleting attachments.") Paperclip.log("Deleting attachments.")
each_attachment do |name, attachment| each_attachment do |name, attachment|
attachment.send(:queue_existing_for_delete)
attachment.send(:flush_deletes) attachment.send(:flush_deletes)
end end
end end
def prepare_for_destroy
Paperclip.log("Scheduling attachments for deletion.")
each_attachment do |name, attachment|
attachment.send(:queue_existing_for_delete)
end
end
end end
end end
# encoding: utf-8 # encoding: utf-8
require 'uri'
module Paperclip module Paperclip
# The Attachment class manages the files for a given attachment. It saves # The Attachment class manages the files for a given attachment. It saves
# when the model saves, deletes when the model is destroyed, and processes # when the model saves, deletes when the model is destroyed, and processes
...@@ -11,8 +13,10 @@ module Paperclip ...@@ -11,8 +13,10 @@ module Paperclip
:url => "/system/:attachment/:id/:style/:filename", :url => "/system/:attachment/:id/:style/:filename",
:path => ":rails_root/public:url", :path => ":rails_root/public:url",
:styles => {}, :styles => {},
:only_process => [],
:processors => [:thumbnail], :processors => [:thumbnail],
:convert_options => {}, :convert_options => {},
:source_file_options => {},
:default_url => "/:attachment/:style/missing.png", :default_url => "/:attachment/:style/missing.png",
:default_style => :original, :default_style => :original,
:storage => :filesystem, :storage => :filesystem,
...@@ -20,62 +24,62 @@ module Paperclip ...@@ -20,62 +24,62 @@ module Paperclip
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails], :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
:use_default_time_zone => true, :use_default_time_zone => true,
:hash_digest => "SHA1", :hash_digest => "SHA1",
:hash_data => ":class/:attachment/:id/:style/:updated_at" :hash_data => ":class/:attachment/:id/:style/:updated_at",
:preserve_files => false
} }
end end
attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options, :interpolator
attr_accessor :post_processing attr_accessor :post_processing
# Creates an Attachment object. +name+ is the name of the attachment, # Creates an Attachment object. +name+ is the name of the attachment,
# +instance+ is the ActiveRecord object instance it's attached to, and # +instance+ is the ActiveRecord object instance it's attached to, and
# +options+ is the same as the hash passed to +has_attached_file+. # +options+ is the same as the hash passed to +has_attached_file+.
#
# Options include:
#
# +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
# +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
# +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
# +only_process+ - style args to be run through the post-processor. This defaults to the empty list
# +default_url+ - a URL for the missing image
# +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
# +storage+ - the storage mechanism. Defaults to :filesystem
# +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
# +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
# +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
# +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
# +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
# +hash_secret+ - a secret passed to the +hash_digest+
# +convert_options+ - flags passed to the +convert+ command for processing
# +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
# +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
# +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
# +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
def initialize name, instance, options = {} def initialize name, instance, options = {}
@name = name @name = name
@instance = instance @instance = instance
options = self.class.default_options.merge(options) options = self.class.default_options.merge(options)
@url = options[:url] @options = Paperclip::Options.new(self, options)
@url = @url.call(self) if @url.is_a?(Proc)
@path = options[:path]
@path = @path.call(self) if @path.is_a?(Proc)
@styles = options[:styles]
@normalized_styles = nil
@default_url = options[:default_url]
@default_style = options[:default_style]
@storage = options[:storage]
@use_timestamp = options[:use_timestamp]
@whiny = options[:whiny_thumbnails] || options[:whiny]
@use_default_time_zone = options[:use_default_time_zone]
@hash_digest = options[:hash_digest]
@hash_data = options[:hash_data]
@hash_secret = options[:hash_secret]
@convert_options = options[:convert_options]
@processors = options[:processors]
@options = options
@post_processing = true @post_processing = true
@queued_for_delete = [] @queued_for_delete = []
@queued_for_write = {} @queued_for_write = {}
@errors = {} @errors = {}
@dirty = false @dirty = false
@interpolator = (options[:interpolator] || Paperclip::Interpolations)
initialize_storage initialize_storage
end end
def styles # [:url, :path, :only_process, :normalized_styles, :default_url, :default_style,
unless @normalized_styles # :storage, :use_timestamp, :whiny, :use_default_time_zone, :hash_digest, :hash_secret,
@normalized_styles = {} # :convert_options, :preserve_files].each do |field|
(@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args| # define_method field do
@normalized_styles[name] = Paperclip::Style.new(name, args.dup, self) # @options.send(field)
end # end
end # end
@normalized_styles
end
def processors
@processors.respond_to?(:call) ? @processors.call(instance) : @processors
end
# What gets called when you call instance.attachment = File. It clears # What gets called when you call instance.attachment = File. It clears
# errors, assigns attributes, and processes the file. It # errors, assigns attributes, and processes the file. It
...@@ -87,6 +91,7 @@ module Paperclip ...@@ -87,6 +91,7 @@ module Paperclip
ensure_required_accessors! ensure_required_accessors!
if uploaded_file.is_a?(Paperclip::Attachment) if uploaded_file.is_a?(Paperclip::Attachment)
uploaded_filename = uploaded_file.original_filename
uploaded_file = uploaded_file.to_file(:original) uploaded_file = uploaded_file.to_file(:original)
close_uploaded_file = uploaded_file.respond_to?(:close) close_uploaded_file = uploaded_file.respond_to?(:close)
else else
...@@ -100,8 +105,9 @@ module Paperclip ...@@ -100,8 +105,9 @@ module Paperclip
return nil if uploaded_file.nil? return nil if uploaded_file.nil?
uploaded_filename ||= uploaded_file.original_filename
@queued_for_write[:original] = to_tempfile(uploaded_file) @queued_for_write[:original] = to_tempfile(uploaded_file)
instance_write(:file_name, uploaded_file.original_filename.strip) instance_write(:file_name, uploaded_filename.strip)
instance_write(:content_type, uploaded_file.content_type.to_s.strip) instance_write(:content_type, uploaded_file.content_type.to_s.strip)
instance_write(:file_size, uploaded_file.size.to_i) instance_write(:file_size, uploaded_file.size.to_i)
instance_write(:fingerprint, generate_fingerprint(uploaded_file)) instance_write(:fingerprint, generate_fingerprint(uploaded_file))
...@@ -109,7 +115,7 @@ module Paperclip ...@@ -109,7 +115,7 @@ module Paperclip
@dirty = true @dirty = true
post_process if @post_processing post_process(*@options.only_process) if post_processing
# Reset the file size if the original file was reprocessed. # Reset the file size if the original file was reprocessed.
instance_write(:file_size, @queued_for_write[:original].size.to_i) instance_write(:file_size, @queued_for_write[:original].size.to_i)
...@@ -124,17 +130,21 @@ module Paperclip ...@@ -124,17 +130,21 @@ module Paperclip
# grained security. This is not recommended if you don't need the # grained security. This is not recommended if you don't need the
# security, however, for performance reasons. Set use_timestamp to false # security, however, for performance reasons. Set use_timestamp to false
# if you want to stop the attachment update time appended to the url # if you want to stop the attachment update time appended to the url
def url(style_name = default_style, use_timestamp = @use_timestamp) def url(style_name = default_style, use_timestamp = @options.use_timestamp)
url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name) default_url = @options.default_url.is_a?(Proc) ? @options.default_url.call(self) : @options.default_url
use_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url url = original_filename.nil? ? interpolate(default_url, style_name) : interpolate(@options.url, style_name)
url << (url.include?("?") ? "&" : "?") + updated_at.to_s if use_timestamp && updated_at
url.respond_to?(:escape) ? url.escape : URI.escape(url)
end end
# Returns the path of the attachment as defined by the :path option. If the # Returns the path of the attachment as defined by the :path option. If the
# file is stored in the filesystem the path refers to the path of the file # file is stored in the filesystem the path refers to the path of the file
# on disk. If the file is stored in S3, the path is the "key" part of the # on disk. If the file is stored in S3, the path is the "key" part of the
# URL, and the :bucket option refers to the S3 bucket. # URL, and the :bucket option refers to the S3 bucket.
def path style_name = default_style def path(style_name = default_style)
original_filename.nil? ? nil : interpolate(@path, style_name) path = original_filename.nil? ? nil : interpolate(@options.path, style_name)
path.respond_to?(:unescape) ? path.unescape : path
end end
# Alias to +url+ # Alias to +url+
...@@ -142,6 +152,14 @@ module Paperclip ...@@ -142,6 +152,14 @@ module Paperclip
url(style_name) url(style_name)
end end
def default_style
@options.default_style
end
def styles
@options.styles
end
# Returns an array containing the errors on this attachment. # Returns an array containing the errors on this attachment.
def errors def errors
@errors @errors
...@@ -166,6 +184,7 @@ module Paperclip ...@@ -166,6 +184,7 @@ module Paperclip
# use #destroy. # use #destroy.
def clear def clear
queue_existing_for_delete queue_existing_for_delete
@queued_for_write = {}
@errors = {} @errors = {}
end end
...@@ -173,8 +192,10 @@ module Paperclip ...@@ -173,8 +192,10 @@ module Paperclip
# nil to the attachment *and saving*. This is permanent. If you wish to # nil to the attachment *and saving*. This is permanent. If you wish to
# wipe out the existing attachment but not save, use #clear. # wipe out the existing attachment but not save, use #clear.
def destroy def destroy
clear unless @options.preserve_files
save clear
save
end
end end
# Returns the uploaded file if present. # Returns the uploaded file if present.
...@@ -216,22 +237,26 @@ module Paperclip ...@@ -216,22 +237,26 @@ module Paperclip
# The time zone to use for timestamp interpolation. Using the default # The time zone to use for timestamp interpolation. Using the default
# time zone ensures that results are consistent across all threads. # time zone ensures that results are consistent across all threads.
def time_zone def time_zone
@use_default_time_zone ? Time.zone_default : Time.zone @options.use_default_time_zone ? Time.zone_default : Time.zone
end end
# Returns a unique hash suitable for obfuscating the URL of an otherwise # Returns a unique hash suitable for obfuscating the URL of an otherwise
# publicly viewable attachment. # publicly viewable attachment.
def hash(style_name = default_style) def hash(style_name = default_style)
raise ArgumentError, "Unable to generate hash without :hash_secret" unless @hash_secret raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options.hash_secret
require 'openssl' unless defined?(OpenSSL) require 'openssl' unless defined?(OpenSSL)
data = interpolate(@hash_data, style_name) data = interpolate(@options.hash_data, style_name)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@hash_digest).new, @hash_secret, data) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options.hash_digest).new, @options.hash_secret, data)
end end
def generate_fingerprint(source) def generate_fingerprint(source)
data = source.read if source.respond_to?(:path) && source.path && !source.path.blank?
source.rewind if source.respond_to?(:rewind) Digest::MD5.file(source.path).to_s
Digest::MD5.hexdigest(data) else
data = source.read
source.rewind if source.respond_to?(:rewind)
Digest::MD5.hexdigest(data)
end
end end
# Paths and URLs can have a number of variables interpolated into them # Paths and URLs can have a number of variables interpolated into them
...@@ -258,9 +283,11 @@ module Paperclip ...@@ -258,9 +283,11 @@ module Paperclip
new_original.rewind new_original.rewind
@queued_for_write = { :original => new_original } @queued_for_write = { :original => new_original }
instance_write(:updated_at, Time.now)
post_process(*style_args) post_process(*style_args)
old_original.close if old_original.respond_to?(:close) old_original.close if old_original.respond_to?(:close)
old_original.unlink if old_original.respond_to?(:unlink)
save save
else else
...@@ -276,6 +303,8 @@ module Paperclip ...@@ -276,6 +303,8 @@ module Paperclip
!original_filename.blank? !original_filename.blank?
end end
alias :present? :file?
# Writes the attachment-specific attribute on the instance. For example, # Writes the attachment-specific attribute on the instance. For example,
# instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
# "avatar_file_name" field (assuming the attachment is called avatar). # "avatar_file_name" field (assuming the attachment is called avatar).
...@@ -315,19 +344,28 @@ module Paperclip ...@@ -315,19 +344,28 @@ module Paperclip
end end
def initialize_storage #:nodoc: def initialize_storage #:nodoc:
storage_class_name = @storage.to_s.capitalize storage_class_name = @options.storage.to_s.downcase.camelize
begin begin
@storage_module = Paperclip::Storage.const_get(storage_class_name) storage_module = Paperclip::Storage.const_get(storage_class_name)
rescue NameError rescue NameError
raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'" raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
end end
self.extend(@storage_module) self.extend(storage_module)
end end
def extra_options_for(style) #:nodoc: def extra_options_for(style) #:nodoc:
all_options = convert_options[:all] all_options = @options.convert_options[:all]
all_options = all_options.call(instance) if all_options.respond_to?(:call) all_options = all_options.call(instance) if all_options.respond_to?(:call)
style_options = convert_options[style] style_options = @options.convert_options[style]
style_options = style_options.call(instance) if style_options.respond_to?(:call)
[ style_options, all_options ].compact.join(" ")
end
def extra_source_file_options_for(style) #:nodoc:
all_options = @options.source_file_options[:all]
all_options = all_options.call(instance) if all_options.respond_to?(:call)
style_options = @options.source_file_options[style]
style_options = style_options.call(instance) if style_options.respond_to?(:call) style_options = style_options.call(instance) if style_options.respond_to?(:call)
[ style_options, all_options ].compact.join(" ") [ style_options, all_options ].compact.join(" ")
...@@ -343,7 +381,7 @@ module Paperclip ...@@ -343,7 +381,7 @@ module Paperclip
end end
def post_process_styles(*style_args) #:nodoc: def post_process_styles(*style_args) #:nodoc:
styles.each do |name, style| @options.styles.each do |name, style|
begin begin
if style_args.empty? || style_args.include?(name) if style_args.empty? || style_args.include?(name)
raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank? raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
...@@ -353,18 +391,18 @@ module Paperclip ...@@ -353,18 +391,18 @@ module Paperclip
end end
rescue PaperclipError => e rescue PaperclipError => e
log("An error was received while processing: #{e.inspect}") log("An error was received while processing: #{e.inspect}")
(@errors[:processing] ||= []) << e.message if @whiny (@errors[:processing] ||= []) << e.message if @options.whiny
end end
end end
end end
def interpolate pattern, style_name = default_style #:nodoc: def interpolate(pattern, style_name = default_style) #:nodoc:
Paperclip::Interpolations.interpolate(pattern, self, style_name) interpolator.interpolate(pattern, self, style_name)
end end
def queue_existing_for_delete #:nodoc: def queue_existing_for_delete #:nodoc:
return unless file? return if @options.preserve_files || !file?
@queued_for_delete += [:original, *styles.keys].uniq.map do |style| @queued_for_delete += [:original, *@options.styles.keys].uniq.map do |style|
path(style) if exists?(style) path(style) if exists?(style)
end.compact end.compact
instance_write(:file_name, nil) instance_write(:file_name, nil)
...@@ -379,5 +417,13 @@ module Paperclip ...@@ -379,5 +417,13 @@ module Paperclip
end end
end end
# called by storage after the writes are flushed and before @queued_for_writes is cleared
def after_flush_writes
@queued_for_write.each do |style, file|
file.close unless file.closed?
file.unlink if file.respond_to?(:unlink) && file.path.present? && File.exist?(file.path)
end
end
end end
end end
module Paperclip
class CommandLine
class << self
attr_accessor :path
end
def initialize(binary, params = "", options = {})
@binary = binary.dup
@params = params.dup
@options = options.dup
@swallow_stderr = @options.has_key?(:swallow_stderr) ? @options.delete(:swallow_stderr) : Paperclip.options[:swallow_stderr]
@expected_outcodes = @options.delete(:expected_outcodes)
@expected_outcodes ||= [0]
end
def command
cmd = []
cmd << full_path(@binary)
cmd << interpolate(@params, @options)
cmd << bit_bucket if @swallow_stderr
cmd.join(" ")
end
def run
Paperclip.log(command)
begin
output = self.class.send(:'`', command)
rescue Errno::ENOENT
raise Paperclip::CommandNotFoundError
end
if $?.exitstatus == 127
raise Paperclip::CommandNotFoundError
end
unless @expected_outcodes.include?($?.exitstatus)
raise Paperclip::PaperclipCommandLineError, "Command '#{command}' returned #{$?.exitstatus}. Expected #{@expected_outcodes.join(", ")}"
end
output
end
private
def full_path(binary)
[self.class.path, binary].compact.join("/")
end
def interpolate(pattern, vars)
# interpolates :variables and :{variables}
pattern.gsub(%r#:(?:\w+|\{\w+\})#) do |match|
key = match[1..-1]
key = key[1..-2] if key[0,1] == '{'
if invalid_variables.include?(key)
raise PaperclipCommandLineError,
"Interpolation of #{key} isn't allowed."
end
interpolation(vars, key) || match
end
end
def invalid_variables
%w(expected_outcodes swallow_stderr)
end
def interpolation(vars, key)
if vars.key?(key.to_sym)
shell_quote(vars[key.to_sym])
end
end
def shell_quote(string)
return "" if string.nil? or string.blank?
if self.class.unix?
string.split("'").map{|m| "'#{m}'" }.join("\\'")
else
%{"#{string}"}
end
end
def bit_bucket
self.class.unix? ? "2>/dev/null" : "2>NUL"
end
def self.unix?
File.exist?("/dev/null")
end
end
end
...@@ -13,15 +13,20 @@ module Paperclip ...@@ -13,15 +13,20 @@ module Paperclip
# Uses ImageMagick to determing the dimensions of a file, passed in as either a # Uses ImageMagick to determing the dimensions of a file, passed in as either a
# File or path. # File or path.
# NOTE: (race cond) Do not reassign the 'file' variable inside this method as it is likely to be
# a Tempfile object, which would be eligible for file deletion when no longer referenced.
def self.from_file file def self.from_file file
file = file.path if file.respond_to? "path" file_path = file.respond_to?(:path) ? file.path : file
raise(Paperclip::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")) if file_path.blank?
geometry = begin geometry = begin
Paperclip.run("identify", "-format %wx%h :file", :file => "#{file}[0]") Paperclip.run("identify", "-format %wx%h :file", :file => "#{file_path}[0]")
rescue PaperclipCommandLineError rescue Cocaine::ExitStatusError
"" ""
rescue Cocaine::CommandNotFoundError => e
raise Paperclip::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
end end
parse(geometry) || parse(geometry) ||
raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command.")) raise(NotIdentifiedByImageMagickError.new("#{file_path} is not recognized by the 'identify' command."))
end end
# Parses a "WxH" formatted string, where W is the width and H is the height. # Parses a "WxH" formatted string, where W is the width and H is the height.
......
require 'uri'
module Paperclip
class InterpolatedString < String
def escaped?
!!@escaped
end
def escape
if !escaped?
escaped_string = self.class.new(URI.escape(self))
escaped_string.instance_variable_set(:@escaped, true)
escaped_string
else
self
end
end
def unescape
if escaped?
escaped_string = self.class.new(URI.unescape(self))
escaped_string.instance_variable_set(:@escaped, false)
escaped_string
else
self
end
end
def force_escape
@escaped = true
end
end
end
require 'paperclip/interpolated_string'
module Paperclip module Paperclip
# This module contains all the methods that are available for interpolation # This module contains all the methods that are available for interpolation
# in paths and urls. To add your own (or override an existing one), you # in paths and urls. To add your own (or override an existing one), you
...@@ -6,13 +8,13 @@ module Paperclip ...@@ -6,13 +8,13 @@ module Paperclip
module Interpolations module Interpolations
extend self extend self
# Hash assignment of interpolations. Included only for compatability, # Hash assignment of interpolations. Included only for compatibility,
# and is not intended for normal use. # and is not intended for normal use.
def self.[]= name, block def self.[]= name, block
define_method(name, &block) define_method(name, &block)
end end
# Hash access of interpolations. Included only for compatability, # Hash access of interpolations. Included only for compatibility,
# and is not intended for normal use. # and is not intended for normal use.
def self.[] name def self.[] name
method(name) method(name)
...@@ -25,17 +27,22 @@ module Paperclip ...@@ -25,17 +27,22 @@ module Paperclip
# Perform the actual interpolation. Takes the pattern to interpolate # Perform the actual interpolation. Takes the pattern to interpolate
# and the arguments to pass, which are the attachment and style name. # and the arguments to pass, which are the attachment and style name.
# You can pass a method name on your record as a symbol, which should turn
# an interpolation pattern for Paperclip to use.
def self.interpolate pattern, *args def self.interpolate pattern, *args
all.reverse.inject( pattern.dup ) do |result, tag| pattern = args.first.instance.send(pattern) if pattern.kind_of? Symbol
interpolated_string = all.reverse.inject(InterpolatedString.new(pattern)) do |result, tag|
result.gsub(/:#{tag}/) do |match| result.gsub(/:#{tag}/) do |match|
send( tag, *args ) send( tag, *args )
end end
end end
interpolated_string.force_escape if pattern =~ /:url/
interpolated_string
end end
# Returns the filename, the same way as ":basename.:extension" would. # Returns the filename, the same way as ":basename.:extension" would.
def filename attachment, style_name def filename attachment, style_name
"#{basename(attachment, style_name)}.#{extension(attachment, style_name)}" [ basename(attachment, style_name), extension(attachment, style_name) ].reject(&:blank?).join(".")
end end
# Returns the interpolated URL. Will raise an error if the url itself # Returns the interpolated URL. Will raise an error if the url itself
...@@ -83,7 +90,7 @@ module Paperclip ...@@ -83,7 +90,7 @@ module Paperclip
# Returns the basename of the file. e.g. "file" for "file.jpg" # Returns the basename of the file. e.g. "file" for "file.jpg"
def basename attachment, style_name def basename attachment, style_name
attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "") attachment.original_filename.gsub(/#{Regexp.escape(File.extname(attachment.original_filename))}$/, "")
end end
# Returns the extension of the file. e.g. "jpg" for "file.jpg" # Returns the extension of the file. e.g. "jpg" for "file.jpg"
...@@ -94,11 +101,41 @@ module Paperclip ...@@ -94,11 +101,41 @@ module Paperclip
File.extname(attachment.original_filename).gsub(/^\.+/, "") File.extname(attachment.original_filename).gsub(/^\.+/, "")
end end
# Returns an extension based on the content type. e.g. "jpeg" for "image/jpeg".
# Each mime type generally has multiple extensions associated with it, so
# if the extension from teh original filename is one of these extensions,
# that extension is used, otherwise, the first in the list is used.
def content_type_extension attachment, style_name
mime_type = MIME::Types[attachment.content_type]
extensions_for_mime_type = unless mime_type.empty?
mime_type.first.extensions
else
[]
end
original_extension = extension(attachment, style_name)
if extensions_for_mime_type.include? original_extension
original_extension
elsif !extensions_for_mime_type.empty?
extensions_for_mime_type.first
else
# It's possible, though unlikely, that the mime type is not in the
# database, so just use the part after the '/' in the mime type as the
# extension.
%r{/([^/]*)$}.match(attachment.content_type)[1]
end
end
# Returns the id of the instance. # Returns the id of the instance.
def id attachment, style_name def id attachment, style_name
attachment.instance.id attachment.instance.id
end end
# Returns the #to_param of the instance.
def param attachment, style_name
attachment.instance.to_param
end
# Returns the fingerprint of the instance. # Returns the fingerprint of the instance.
def fingerprint attachment, style_name def fingerprint attachment, style_name
attachment.fingerprint attachment.fingerprint
...@@ -106,14 +143,22 @@ module Paperclip ...@@ -106,14 +143,22 @@ module Paperclip
# Returns a the attachment hash. See Paperclip::Attachment#hash for # Returns a the attachment hash. See Paperclip::Attachment#hash for
# more details. # more details.
def hash attachment, style_name def hash attachment=nil, style_name=nil
attachment.hash(style_name) if attachment && style_name
attachment.hash(style_name)
else
super()
end
end end
# Returns the id of the instance in a split path form. e.g. returns # Returns the id of the instance in a split path form. e.g. returns
# 000/001/234 for an id of 1234. # 000/001/234 for an id of 1234.
def id_partition attachment, style_name def id_partition attachment, style_name
("%09d" % attachment.instance.id).scan(/\d{3}/).join("/") if (id = attachment.instance.id).is_a?(Integer)
("%09d" % id).scan(/\d{3}/).join("/")
else
id.scan(/.{3}/).first(3).join("/")
end
end end
# Returns the pluralized form of the attachment name. e.g. # Returns the pluralized form of the attachment name. e.g.
......
...@@ -30,7 +30,7 @@ module IOStream ...@@ -30,7 +30,7 @@ module IOStream
end end
# Corrects a bug in Windows when asking for Tempfile size. # Corrects a bug in Windows when asking for Tempfile size.
if defined? Tempfile if defined?(Tempfile) && RUBY_PLATFORM !~ /java/
class Tempfile class Tempfile
def size def size
if @tmpfile if @tmpfile
......
...@@ -17,6 +17,8 @@ module Paperclip ...@@ -17,6 +17,8 @@ module Paperclip
class ValidateAttachmentContentTypeMatcher class ValidateAttachmentContentTypeMatcher
def initialize attachment_name def initialize attachment_name
@attachment_name = attachment_name @attachment_name = attachment_name
@allowed_types = []
@rejected_types = []
end end
def allowing *types def allowing *types
...@@ -37,13 +39,19 @@ module Paperclip ...@@ -37,13 +39,19 @@ module Paperclip
end end
def failure_message def failure_message
"Content types #{@allowed_types.join(", ")} should be accepted" + "".tap do |str|
" and #{@rejected_types.join(", ")} rejected by #{@attachment_name}" str << "Content types #{@allowed_types.join(", ")} should be accepted" if @allowed_types.present?
str << "\n" if @allowed_types.present? && @rejected_types.present?
str << "Content types #{@rejected_types.join(", ")} should be rejected by #{@attachment_name}" if @rejected_types.present?
end
end end
def negative_failure_message def negative_failure_message
"Content types #{@allowed_types.join(", ")} should be rejected" + "".tap do |str|
" and #{@rejected_types.join(", ")} accepted by #{@attachment_name}" str << "Content types #{@allowed_types.join(", ")} should be rejected" if @allowed_types.present?
str << "\n" if @allowed_types.present? && @rejected_types.present?
str << "Content types #{@rejected_types.join(", ")} should be accepted by #{@attachment_name}" if @rejected_types.present?
end
end end
def description def description
...@@ -52,22 +60,20 @@ module Paperclip ...@@ -52,22 +60,20 @@ module Paperclip
protected protected
def allow_types?(types) def type_allowed?(type)
types.all? do |type| file = StringIO.new(".")
file = StringIO.new(".") file.content_type = type
file.content_type = type (subject = @subject.new).attachment_for(@attachment_name).assign(file)
(subject = @subject.new).attachment_for(@attachment_name).assign(file) subject.valid?
subject.valid? subject.errors[:"#{@attachment_name}_content_type"].blank?
subject.errors[:"#{@attachment_name}_content_type"].blank?
end
end end
def allowed_types_allowed? def allowed_types_allowed?
allow_types?(@allowed_types) @allowed_types.all? { |type| type_allowed?(type) }
end end
def rejected_types_rejected? def rejected_types_rejected?
not allow_types?(@rejected_types) !@rejected_types.any? { |type| type_allowed?(type) }
end end
end end
end end
......
require 'set'
module Paperclip
class << self
attr_accessor :classes_with_attachments
attr_writer :registered_attachments_styles_path
def registered_attachments_styles_path
@registered_attachments_styles_path ||= Rails.root.join('public/system/paperclip_attachments.yml').to_s
end
end
self.classes_with_attachments = Set.new
# Get list of styles saved on previous deploy (running rake paperclip:refresh:missing_styles)
def self.get_registered_attachments_styles
YAML.load_file(Paperclip.registered_attachments_styles_path)
rescue Errno::ENOENT
nil
end
private_class_method :get_registered_attachments_styles
def self.save_current_attachments_styles!
File.open(Paperclip.registered_attachments_styles_path, 'w') do |f|
YAML.dump(current_attachments_styles, f)
end
end
# Returns hash with styles for all classes using Paperclip.
# Unfortunately current version does not work with lambda styles:(
# {
# :User => {:avatar => [:small, :big]},
# :Book => {
# :cover => [:thumb, :croppable]},
# :sample => [:thumb, :big]},
# }
# }
def self.current_attachments_styles
Hash.new.tap do |current_styles|
Paperclip.classes_with_attachments.each do |klass_name|
klass = Paperclip.class_for(klass_name)
klass.attachment_definitions.each do |attachment_name, attachment_attributes|
# TODO: is it even possible to take into account Procs?
next if attachment_attributes[:styles].kind_of?(Proc)
attachment_attributes[:styles].try(:keys).try(:each) do |style_name|
klass_sym = klass.to_s.to_sym
current_styles[klass_sym] ||= Hash.new
current_styles[klass_sym][attachment_name.to_sym] ||= Array.new
current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym
current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
end
end
end
end
end
private_class_method :current_attachments_styles
# Returns hash with styles missing from recent run of rake paperclip:refresh:missing_styles
# {
# :User => {:avatar => [:big]},
# :Book => {
# :cover => [:croppable]},
# }
# }
def self.missing_attachments_styles
current_styles = current_attachments_styles
registered_styles = get_registered_attachments_styles
Hash.new.tap do |missing_styles|
current_styles.each do |klass, attachment_definitions|
attachment_definitions.each do |attachment_name, styles|
registered = registered_styles[klass][attachment_name] rescue []
missed = styles - registered
if missed.present?
klass_sym = klass.to_s.to_sym
missing_styles[klass_sym] ||= Hash.new
missing_styles[klass_sym][attachment_name.to_sym] ||= Array.new
missing_styles[klass_sym][attachment_name.to_sym].concat(missed.to_a)
missing_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
end
end
end
end
end
end
module Paperclip
class Options
attr_accessor :url, :path, :only_process, :normalized_styles, :default_url, :default_style,
:storage, :use_timestamp, :whiny, :use_default_time_zone, :hash_digest, :hash_secret,
:convert_options, :source_file_options, :preserve_files, :http_proxy
attr_accessor :s3_credentials, :s3_host_name, :s3_options, :s3_permissions, :s3_protocol,
:s3_headers, :s3_host_alias, :bucket
attr_accessor :fog_directory, :fog_credentials, :fog_host, :fog_public, :fog_file
def initialize(attachment, hash)
@attachment = attachment
@url = hash[:url]
@url = @url.call(@attachment) if @url.is_a?(Proc)
@path = hash[:path]
@path = @path.call(@attachment) if @path.is_a?(Proc)
@styles = hash[:styles]
@only_process = hash[:only_process]
@normalized_styles = nil
@default_url = hash[:default_url]
@default_style = hash[:default_style]
@storage = hash[:storage]
@use_timestamp = hash[:use_timestamp]
@whiny = hash[:whiny_thumbnails] || hash[:whiny]
@use_default_time_zone = hash[:use_default_time_zone]
@hash_digest = hash[:hash_digest]
@hash_data = hash[:hash_data]
@hash_secret = hash[:hash_secret]
@convert_options = hash[:convert_options]
@source_file_options = hash[:source_file_options]
@processors = hash[:processors]
@preserve_files = hash[:preserve_files]
@http_proxy = hash[:http_proxy]
#s3 options
@s3_credentials = hash[:s3_credentials]
@s3_host_name = hash[:s3_host_name]
@bucket = hash[:bucket]
@s3_options = hash[:s3_options]
@s3_permissions = hash[:s3_permissions]
@s3_protocol = hash[:s3_protocol]
@s3_headers = hash[:s3_headers]
@s3_host_alias = hash[:s3_host_alias]
#fog options
@fog_directory = hash[:fog_directory]
@fog_credentials = hash[:fog_credentials]
@fog_host = hash[:fog_host]
@fog_public = hash[:fog_public]
@fog_file = hash[:fog_file]
end
def method_missing(method, *args, &blk)
if method.to_s[-1,1] == "="
instance_variable_set("@#{method[0..-2]}", args[0])
else
instance_variable_get("@#{method}")
end
end
def processors
@processors.respond_to?(:call) ? @processors.call(@attachment.instance) : @processors
end
def styles
if @styles.respond_to?(:call) || !@normalized_styles
@normalized_styles = ActiveSupport::OrderedHash.new
(@styles.respond_to?(:call) ? @styles.call(@attachment) : @styles).each do |name, args|
normalized_styles[name] = Paperclip::Style.new(name, args.dup, @attachment)
end
end
@normalized_styles
end
end
end
...@@ -41,7 +41,7 @@ module Paperclip ...@@ -41,7 +41,7 @@ module Paperclip
# http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
class Tempfile < ::Tempfile class Tempfile < ::Tempfile
# This is Ruby 1.8.7's implementation. # This is Ruby 1.8.7's implementation.
if RUBY_VERSION <= "1.8.6" if RUBY_VERSION <= "1.8.6" || RUBY_PLATFORM =~ /java/
def make_tmpname(basename, n) def make_tmpname(basename, n)
case basename case basename
when Array when Array
......
...@@ -19,6 +19,8 @@ module Paperclip ...@@ -19,6 +19,8 @@ module Paperclip
def self.insert def self.insert
ActiveRecord::Base.send(:include, Paperclip::Glue) ActiveRecord::Base.send(:include, Paperclip::Glue)
File.send(:include, Paperclip::Upfile) File.send(:include, Paperclip::Upfile)
Paperclip.options[:logger] = defined?(ActiveRecord) ? ActiveRecord::Base.logger : Rails.logger
end end
end end
end end
require "paperclip/storage/filesystem" require "paperclip/storage/filesystem"
require "paperclip/storage/fog"
require "paperclip/storage/s3" require "paperclip/storage/s3"
...@@ -38,9 +38,17 @@ module Paperclip ...@@ -38,9 +38,17 @@ module Paperclip
file.close file.close
FileUtils.mkdir_p(File.dirname(path(style_name))) FileUtils.mkdir_p(File.dirname(path(style_name)))
log("saving #{path(style_name)}") log("saving #{path(style_name)}")
FileUtils.mv(file.path, path(style_name)) begin
FileUtils.chmod(0644, path(style_name)) FileUtils.mv(file.path, path(style_name))
rescue SystemCallError
FileUtils.cp(file.path, path(style_name))
FileUtils.rm(file.path)
end
FileUtils.chmod(0666&~File.umask, path(style_name))
end end
after_flush_writes # allows attachment to clean up temp files
@queued_for_write = {} @queued_for_write = {}
end end
...@@ -58,7 +66,7 @@ module Paperclip ...@@ -58,7 +66,7 @@ module Paperclip
FileUtils.rmdir(path) FileUtils.rmdir(path)
break if File.exists?(path) # Ruby 1.9.2 does not raise if the removal failed. break if File.exists?(path) # Ruby 1.9.2 does not raise if the removal failed.
end end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
# Stop trying to remove parent directories # Stop trying to remove parent directories
rescue SystemCallError => e rescue SystemCallError => e
log("There was an unexpected error while deleting directories: #{e.class}") log("There was an unexpected error while deleting directories: #{e.class}")
......
module Paperclip
module Storage
# fog is a modern and versatile cloud computing library for Ruby.
# Among others, it supports Amazon S3 to store your files. In
# contrast to the outdated AWS-S3 gem it is actively maintained and
# supports multiple locations.
# Amazon's S3 file hosting service is a scalable, easy place to
# store files for distribution. You can find out more about it at
# http://aws.amazon.com/s3 There are a few fog-specific options for
# has_attached_file, which will be explained using S3 as an example:
# * +fog_credentials+: Takes a Hash with your credentials. For S3,
# you can use the following format:
# aws_access_key_id: '<your aws_access_key_id>'
# aws_secret_access_key: '<your aws_secret_access_key>'
# provider: 'AWS'
# region: 'eu-west-1'
# * +fog_directory+: This is the name of the S3 bucket that will
# store your files. Remember that the bucket must be unique across
# all of Amazon S3. If the bucket does not exist, Paperclip will
# attempt to create it.
# * +path+: This is the key under the bucket in which the file will
# be stored. The URL will be constructed from the bucket and the
# path. This is what you will want to interpolate. Keys should be
# unique, like filenames, and despite the fact that S3 (strictly
# speaking) does not support directories, you can still use a / to
# separate parts of your file name.
# * +fog_public+: (optional, defaults to true) Should the uploaded
# files be public or not? (true/false)
# * +fog_host+: (optional) The fully-qualified domain name (FQDN)
# that is the alias to the S3 domain of your bucket, e.g.
# 'http://images.example.com'. This can also be used in
# conjunction with Cloudfront (http://aws.amazon.com/cloudfront)
module Fog
def self.extended base
begin
require 'fog'
rescue LoadError => e
e.message << " (You may need to install the fog gem)"
raise e
end unless defined?(Fog)
base.instance_eval do
unless @options.url.to_s.match(/^:fog.*url$/)
@options.path = @options.path.gsub(/:url/, @options.url)
@options.url = ':fog_public_url'
end
Paperclip.interpolates(:fog_public_url) do |attachment, style|
attachment.public_url(style)
end unless Paperclip::Interpolations.respond_to? :fog_public_url
end
end
def exists?(style = default_style)
if original_filename
!!directory.files.head(path(style))
else
false
end
end
def fog_credentials
@fog_credentials ||= parse_credentials(@options.fog_credentials)
end
def fog_file
@fog_file ||= @options.fog_file || {}
end
def fog_public
@fog_public ||= @options.fog_public || true
end
def flush_writes
for style, file in @queued_for_write do
log("saving #{path(style)}")
retried = false
begin
directory.files.create(fog_file.merge(
:body => file,
:key => path(style),
:public => fog_public
))
rescue Excon::Errors::NotFound
raise if retried
retried = true
directory.save
retry
end
end
after_flush_writes # allows attachment to clean up temp files
@queued_for_write = {}
end
def flush_deletes
for path in @queued_for_delete do
log("deleting #{path}")
directory.files.new(:key => path).destroy
end
@queued_for_delete = []
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file(style = default_style)
if @queued_for_write[style]
@queued_for_write[style]
else
body = directory.files.get(path(style)).body
filename = path(style)
extname = File.extname(filename)
basename = File.basename(filename, extname)
file = Tempfile.new([basename, extname])
file.binmode
file.write(body)
file.rewind
file
end
end
def public_url(style = default_style)
if @options.fog_host
host = (@options.fog_host =~ /%d/) ? @options.fog_host % (path(style).hash % 4) : @options.fog_host
"#{host}/#{path(style)}"
else
directory.files.new(:key => path(style)).public_url
end
end
def parse_credentials(creds)
creds = find_credentials(creds).stringify_keys
env = Object.const_defined?(:Rails) ? Rails.env : nil
(creds[env] || creds).symbolize_keys
end
private
def find_credentials(creds)
case creds
when File
YAML::load(ERB.new(File.read(creds.path)).result)
when String, Pathname
YAML::load(ERB.new(File.read(creds)).result)
when Hash
creds
else
raise ArgumentError, "Credentials are not a path, file, or hash."
end
end
def connection
@connection ||= ::Fog::Storage.new(fog_credentials)
end
def directory
@directory ||= connection.directories.new(:key => @options.fog_directory)
end
end
end
end
...@@ -25,8 +25,16 @@ module Paperclip ...@@ -25,8 +25,16 @@ module Paperclip
# development versus production. # development versus production.
# * +s3_permissions+: This is a String that should be one of the "canned" access # * +s3_permissions+: This is a String that should be one of the "canned" access
# policies that S3 provides (more information can be found here: # policies that S3 provides (more information can be found here:
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies) # http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAccessPolicy.html)
# The default for Paperclip is :public_read. # The default for Paperclip is :public_read.
#
# You can set permission on a per style bases by doing the following:
# :s3_permissions => {
# :original => :private
# }
# Or globaly:
# :s3_permissions => :private
#
# * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
# 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
# default), and 'https' when your :s3_permissions are anything else. # default), and 'https' when your :s3_permissions are anything else.
...@@ -39,9 +47,9 @@ module Paperclip ...@@ -39,9 +47,9 @@ module Paperclip
# * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
# S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
# link in the +url+ entry for more information about S3 domains and buckets. # link in the +url+ entry for more information about S3 domains and buckets.
# * +url+: There are three options for the S3 url. You can choose to have the bucket's name # * +url+: There are four options for the S3 url. You can choose to have the bucket's name
# placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket). # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
# Lastly, you can specify a CNAME (which requires the CNAME to be specified as # You can also specify a CNAME (which requires the CNAME to be specified as
# :s3_alias_url. You can read more about CNAMEs and S3 at # :s3_alias_url. You can read more about CNAMEs and S3 at
# http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
# Normally, this won't matter in the slightest and you can leave the default (which is # Normally, this won't matter in the slightest and you can leave the default (which is
...@@ -50,12 +58,15 @@ module Paperclip ...@@ -50,12 +58,15 @@ module Paperclip
# NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
# :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
# alias, the :bucket parameter is ignored, as the hostname is used as the bucket name # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
# by S3. # by S3. The fourth option for the S3 url is :asset_host, which uses Rails' built-in
# asset_host settings. NOTE: To get the full url from a paperclip'd object, use the
# image_path helper; this is what image_tag uses to generate the url for an img tag.
# * +path+: This is the key under the bucket in which the file will be stored. The # * +path+: This is the key under the bucket in which the file will be stored. The
# URL will be constructed from the bucket and the path. This is what you will want # URL will be constructed from the bucket and the path. This is what you will want
# to interpolate. Keys should be unique, like filenames, and despite the fact that # to interpolate. Keys should be unique, like filenames, and despite the fact that
# S3 (strictly speaking) does not support directories, you can still use a / to # S3 (strictly speaking) does not support directories, you can still use a / to
# separate parts of your file name. # separate parts of your file name.
# * +s3_host_name+: If you are using your bucket in Tokyo region etc, write host_name.
module S3 module S3
def self.extended base def self.extended base
begin begin
...@@ -66,49 +77,101 @@ module Paperclip ...@@ -66,49 +77,101 @@ module Paperclip
end unless defined?(AWS::S3) end unless defined?(AWS::S3)
base.instance_eval do base.instance_eval do
@s3_credentials = parse_credentials(@options[:s3_credentials]) @s3_options = @options.s3_options || {}
@bucket = @options[:bucket] || @s3_credentials[:bucket] @s3_permissions = set_permissions(@options.s3_permissions)
@bucket = @bucket.call(self) if @bucket.is_a?(Proc) @s3_protocol = @options.s3_protocol ||
@s3_options = @options[:s3_options] || {} Proc.new do |style|
@s3_permissions = @options[:s3_permissions] || :public_read (@s3_permissions[style.to_sym] || @s3_permissions[:default]) == :public_read ? 'http' : 'https'
@s3_protocol = @options[:s3_protocol] || (@s3_permissions == :public_read ? 'http' : 'https') end
@s3_headers = @options[:s3_headers] || {} @s3_headers = @options.s3_headers || {}
@s3_host_alias = @options[:s3_host_alias]
unless @url.to_s.match(/^:s3.*url$/) unless @options.url.to_s.match(/^:s3.*url$/) || @options.url == ":asset_host"
@path = @path.gsub(/:url/, @url) @options.path = @options.path.gsub(/:url/, @options.url).gsub(/^:rails_root\/public\/system/, '')
@url = ":s3_path_url" @options.url = ":s3_path_url"
end
@options.url = @options.url.inspect if @options.url.is_a?(Symbol)
@http_proxy = @options.http_proxy || nil
if @http_proxy
@s3_options.merge!({:proxy => @http_proxy})
end end
AWS::S3::Base.establish_connection!( @s3_options.merge( AWS::S3::Base.establish_connection!( @s3_options.merge(
:access_key_id => @s3_credentials[:access_key_id], :access_key_id => s3_credentials[:access_key_id],
:secret_access_key => @s3_credentials[:secret_access_key] :secret_access_key => s3_credentials[:secret_access_key]
)) ))
end end
Paperclip.interpolates(:s3_alias_url) do |attachment, style| Paperclip.interpolates(:s3_alias_url) do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}" "#{attachment.s3_protocol(style)}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
end unless Paperclip::Interpolations.respond_to? :s3_alias_url end unless Paperclip::Interpolations.respond_to? :s3_alias_url
Paperclip.interpolates(:s3_path_url) do |attachment, style| Paperclip.interpolates(:s3_path_url) do |attachment, style|
"#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}" "#{attachment.s3_protocol(style)}://#{attachment.s3_host_name}/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
end unless Paperclip::Interpolations.respond_to? :s3_path_url end unless Paperclip::Interpolations.respond_to? :s3_path_url
Paperclip.interpolates(:s3_domain_url) do |attachment, style| Paperclip.interpolates(:s3_domain_url) do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}" "#{attachment.s3_protocol(style)}://#{attachment.bucket_name}.#{attachment.s3_host_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
end unless Paperclip::Interpolations.respond_to? :s3_domain_url end unless Paperclip::Interpolations.respond_to? :s3_domain_url
Paperclip.interpolates(:asset_host) do |attachment, style|
"#{attachment.path(style).gsub(%r{^/}, "")}"
end unless Paperclip::Interpolations.respond_to? :asset_host
end end
def expiring_url(time = 3600) def expiring_url(time = 3600, style_name = default_style)
AWS::S3::S3Object.url_for(path, bucket_name, :expires_in => time ) AWS::S3::S3Object.url_for(path(style_name), bucket_name, :expires_in => time, :use_ssl => (s3_protocol(style_name) == 'https'))
end end
def bucket_name def s3_credentials
@bucket @s3_credentials ||= parse_credentials(@options.s3_credentials)
end
def s3_host_name
@options.s3_host_name || s3_credentials[:s3_host_name] || "s3.amazonaws.com"
end end
def s3_host_alias def s3_host_alias
@s3_host_alias = @options.s3_host_alias
@s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.is_a?(Proc)
@s3_host_alias @s3_host_alias
end end
def bucket_name
@bucket = @options.bucket || s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
@bucket
end
def using_http_proxy?
!!@http_proxy
end
def http_proxy_host
using_http_proxy? ? @http_proxy[:host] : nil
end
def http_proxy_port
using_http_proxy? ? @http_proxy[:port] : nil
end
def http_proxy_user
using_http_proxy? ? @http_proxy[:user] : nil
end
def http_proxy_password
using_http_proxy? ? @http_proxy[:password] : nil
end
def set_permissions permissions
if permissions.is_a?(Hash)
permissions[:default] = permissions[:default] || :public_read
else
permissions = { :default => permissions || :public_read }
end
permissions
end
def parse_credentials creds def parse_credentials creds
creds = find_credentials(creds).stringify_keys creds = find_credentials(creds).stringify_keys
(creds[Rails.env] || creds).symbolize_keys env = Object.const_defined?(:Rails) ? Rails.env : nil
(creds[env] || creds).symbolize_keys
end end
def exists?(style = default_style) def exists?(style = default_style)
...@@ -119,8 +182,12 @@ module Paperclip ...@@ -119,8 +182,12 @@ module Paperclip
end end
end end
def s3_protocol def s3_protocol(style = default_style)
@s3_protocol if @s3_protocol.is_a?(Proc)
@s3_protocol.call(style)
else
@s3_protocol
end
end end
# Returns representation of the data of the file assigned to the given # Returns representation of the data of the file assigned to the given
...@@ -148,8 +215,8 @@ module Paperclip ...@@ -148,8 +215,8 @@ module Paperclip
AWS::S3::S3Object.store(path(style), AWS::S3::S3Object.store(path(style),
file, file,
bucket_name, bucket_name,
{:content_type => instance_read(:content_type), {:content_type => file.content_type.to_s.strip,
:access => @s3_permissions, :access => (@s3_permissions[style] || @s3_permissions[:default]),
}.merge(@s3_headers)) }.merge(@s3_headers))
rescue AWS::S3::NoSuchBucket => e rescue AWS::S3::NoSuchBucket => e
create_bucket create_bucket
...@@ -158,6 +225,9 @@ module Paperclip ...@@ -158,6 +225,9 @@ module Paperclip
raise raise
end end
end end
after_flush_writes # allows attachment to clean up temp files
@queued_for_write = {} @queued_for_write = {}
end end
......
...@@ -30,13 +30,14 @@ module Paperclip ...@@ -30,13 +30,14 @@ module Paperclip
# (which method (in the attachment) will call any supplied procs) # (which method (in the attachment) will call any supplied procs)
# There is an important change of interface here: a style rule can set its own processors # There is an important change of interface here: a style rule can set its own processors
# by default we behave as before, though. # by default we behave as before, though.
# if a proc has been supplied, we call it here
def processors def processors
@processors || attachment.processors @processors.respond_to?(:call) ? @processors.call(attachment.instance) : (@processors || attachment.options.processors)
end end
# retrieves from the attachment the whiny setting # retrieves from the attachment the whiny setting
def whiny def whiny
attachment.whiny attachment.options.whiny
end end
# returns true if we're inclined to grumble # returns true if we're inclined to grumble
...@@ -48,6 +49,10 @@ module Paperclip ...@@ -48,6 +49,10 @@ module Paperclip
attachment.send(:extra_options_for, name) attachment.send(:extra_options_for, name)
end end
def source_file_options
attachment.send(:extra_source_file_options_for, name)
end
# returns the geometry string for this style # returns the geometry string for this style
# if a proc has been supplied, we call it here # if a proc has been supplied, we call it here
def geometry def geometry
...@@ -62,16 +67,16 @@ module Paperclip ...@@ -62,16 +67,16 @@ module Paperclip
@other_args.each do |k,v| @other_args.each do |k,v|
args[k] = v.respond_to?(:call) ? v.call(attachment) : v args[k] = v.respond_to?(:call) ? v.call(attachment) : v
end end
[:processors, :geometry, :format, :whiny, :convert_options].each do |k| [:processors, :geometry, :format, :whiny, :convert_options, :source_file_options].each do |k|
(arg = send(k)) && args[k] = arg (arg = send(k)) && args[k] = arg
end end
args args
end end
# Supports getting and setting style properties with hash notation to ensure backwards-compatibility # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
# eg. @attachment.styles[:large][:geometry]@ will still work # eg. @attachment.options.styles[:large][:geometry]@ will still work
def [](key) def [](key)
if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key) if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key)
send(key) send(key)
elsif defined? @other_args[key] elsif defined? @other_args[key]
@other_args[key] @other_args[key]
...@@ -79,7 +84,7 @@ module Paperclip ...@@ -79,7 +84,7 @@ module Paperclip
end end
def []=(key, value) def []=(key, value)
if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key) if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key)
send("#{key}=".intern, value) send("#{key}=".intern, value)
else else
@other_args[key] = value @other_args[key] = value
......
...@@ -2,7 +2,11 @@ module Paperclip ...@@ -2,7 +2,11 @@ module Paperclip
# Handles thumbnailing images that are uploaded. # Handles thumbnailing images that are uploaded.
class Thumbnail < Processor class Thumbnail < Processor
attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options, :source_file_options attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options,
:source_file_options, :animated
# List of formats that we need to preserve animation
ANIMATED_FORMATS = %w(gif)
# Creates a Thumbnail object set to work on the +file+ given. It # Creates a Thumbnail object set to work on the +file+ given. It
# will attempt to transform the image into one defined by +target_geometry+ # will attempt to transform the image into one defined by +target_geometry+
...@@ -10,18 +14,30 @@ module Paperclip ...@@ -10,18 +14,30 @@ module Paperclip
# unless specified. Thumbnail creation will raise no errors unless # unless specified. Thumbnail creation will raise no errors unless
# +whiny+ is true (which it is, by default. If +convert_options+ is # +whiny+ is true (which it is, by default. If +convert_options+ is
# set, the options will be appended to the convert command upon image conversion # set, the options will be appended to the convert command upon image conversion
def initialize file, options = {}, attachment = nil #
# Options include:
#
# +geometry+ - the desired width and height of the thumbnail (required)
# +file_geometry_parser+ - an object with a method named +from_file+ that takes an image file and produces its geometry and a +transformation_to+. Defaults to Paperclip::Geometry
# +string_geometry_parser+ - an object with a method named +parse+ that takes a string and produces an object with +width+, +height+, and +to_s+ accessors. Defaults to Paperclip::Geometry
# +source_file_options+ - flags passed to the +convert+ command that influence how the source file is read
# +convert_options+ - flags passed to the +convert+ command that influence how the image is processed
# +whiny+ - whether to raise an error when processing fails. Defaults to true
# +format+ - the desired filename extension
# +animated+ - whether to merge all the layers in the image. Defaults to true
def initialize(file, options = {}, attachment = nil)
super super
geometry = options[:geometry] geometry = options[:geometry] # this is not an option
@file = file @file = file
@crop = geometry[-1,1] == '#' @crop = geometry[-1,1] == '#'
@target_geometry = Geometry.parse geometry @target_geometry = (options[:string_geometry_parser] || Geometry).parse(geometry)
@current_geometry = Geometry.from_file @file @current_geometry = (options[:file_geometry_parser] || Geometry).from_file(@file)
@source_file_options = options[:source_file_options] @source_file_options = options[:source_file_options]
@convert_options = options[:convert_options] @convert_options = options[:convert_options]
@whiny = options[:whiny].nil? ? true : options[:whiny] @whiny = options[:whiny].nil? ? true : options[:whiny]
@format = options[:format] @format = options[:format]
@animated = options[:animated].nil? ? true : options[:animated]
@source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split) @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
@convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split) @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
...@@ -58,9 +74,11 @@ module Paperclip ...@@ -58,9 +74,11 @@ module Paperclip
parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ") parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}[0]", :dest => File.expand_path(dst.path)) success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}#{'[0]' unless animated?}", :dest => File.expand_path(dst.path))
rescue PaperclipCommandLineError => e rescue Cocaine::ExitStatusError => e
raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
rescue Cocaine::CommandNotFoundError => e
raise Paperclip::CommandNotFoundError.new("Could not run the `convert` command. Please install ImageMagick.")
end end
dst dst
...@@ -71,9 +89,17 @@ module Paperclip ...@@ -71,9 +89,17 @@ module Paperclip
def transformation_command def transformation_command
scale, crop = @current_geometry.transformation_to(@target_geometry, crop?) scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
trans = [] trans = []
trans << "-coalesce" if animated?
trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty? trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
trans << "-crop" << %["#{crop}"] << "+repage" if crop trans << "-crop" << %["#{crop}"] << "+repage" if crop
trans trans
end end
protected
# Return true if the format is animated
def animated?
@animated && ANIMATED_FORMATS.include?(@current_format[1..-1]) && (ANIMATED_FORMATS.include?(@format.to_s) || @format.blank?)
end
end end
end end
require 'mime/types'
module Paperclip module Paperclip
# The Upfile module is a convenience module for adding uploaded-file-type methods # The Upfile module is a convenience module for adding uploaded-file-type methods
# to the +File+ class. Useful for testing. # to the +File+ class. Useful for testing.
...@@ -6,23 +8,28 @@ module Paperclip ...@@ -6,23 +8,28 @@ module Paperclip
# Infer the MIME-type of the file from the extension. # Infer the MIME-type of the file from the extension.
def content_type def content_type
type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase types = MIME::Types.type_for(self.original_filename)
case type if types.length == 0
when %r"jp(e|g|eg)" then "image/jpeg" type_from_file_command
when %r"tiff?" then "image/tiff" elsif types.length == 1
when %r"png", "gif", "bmp" then "image/#{type}" types.first.content_type
when "txt" then "text/plain"
when %r"html?" then "text/html"
when "js" then "application/js"
when "csv", "xml", "css" then "text/#{type}"
else else
# On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist. iterate_over_array_to_find_best_option(types)
content_type = (Paperclip.run("file", "-b --mime-type :file", :file => self.path).split(':').last.strip rescue "application/x-#{type}")
content_type = "application/x-#{type}" if content_type.match(/\(.*?\)/)
content_type
end end
end end
def iterate_over_array_to_find_best_option(types)
types.reject {|type| type.content_type.match(/\/x-/) }.first
end
def type_from_file_command
# On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
type = (self.original_filename.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
mime_type = (Paperclip.run("file", "-b --mime :file", :file => self.path).split(/[:;]\s+/)[0] rescue "application/x-#{type}")
mime_type = "application/x-#{type}" if mime_type.match(/\(.*?\)/)
mime_type
end
# Returns the file's normal name. # Returns the file's normal name.
def original_filename def original_filename
File.basename(self.path) File.basename(self.path)
......
module Paperclip module Paperclip
VERSION = "2.3.8" unless defined? Paperclip::VERSION VERSION = "2.4.4" unless defined? Paperclip::VERSION
end end
def obtain_class module Paperclip
class_name = ENV['CLASS'] || ENV['class'] module Task
raise "Must specify CLASS" unless class_name def self.obtain_class
class_name class_name = ENV['CLASS'] || ENV['class']
end raise "Must specify CLASS" unless class_name
class_name
end
def obtain_attachments(klass) def self.obtain_attachments(klass)
klass = Object.const_get(klass.to_s) klass = Paperclip.class_for(klass.to_s)
name = ENV['ATTACHMENT'] || ENV['attachment'] name = ENV['ATTACHMENT'] || ENV['attachment']
raise "Class #{klass.name} has no attachments specified" unless klass.respond_to?(:attachment_definitions) raise "Class #{klass.name} has no attachments specified" unless klass.respond_to?(:attachment_definitions)
if !name.blank? && klass.attachment_definitions.keys.include?(name) if !name.blank? && klass.attachment_definitions.keys.include?(name)
[ name ] [ name ]
else else
klass.attachment_definitions.keys klass.attachment_definitions.keys
end
end
end end
end end
...@@ -20,14 +24,15 @@ namespace :paperclip do ...@@ -20,14 +24,15 @@ namespace :paperclip do
task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"] task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
namespace :refresh do namespace :refresh do
desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)." desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT and STYLES splitted by comma)."
task :thumbnails => :environment do task :thumbnails => :environment do
errors = [] errors = []
klass = obtain_class klass = Paperclip::Task.obtain_class
names = obtain_attachments(klass) names = Paperclip::Task.obtain_attachments(klass)
styles = (ENV['STYLES'] || ENV['styles'] || '').split(',').map(&:to_sym)
names.each do |name| names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance| Paperclip.each_instance_with_attachment(klass, name) do |instance|
result = instance.send(name).reprocess! instance.send(name).reprocess!(*styles)
errors << [instance.id, instance.errors] unless instance.errors.blank? errors << [instance.id, instance.errors] unless instance.errors.blank?
end end
end end
...@@ -36,35 +41,59 @@ namespace :paperclip do ...@@ -36,35 +41,59 @@ namespace :paperclip do
desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)." desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
task :metadata => :environment do task :metadata => :environment do
klass = obtain_class klass = Paperclip::Task.obtain_class
names = obtain_attachments(klass) names = Paperclip::Task.obtain_attachments(klass)
names.each do |name| names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance| Paperclip.each_instance_with_attachment(klass, name) do |instance|
if file = instance.send(name).to_file if file = instance.send(name).to_file(:original)
instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip) instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
instance.send("#{name}_content_type=", file.content_type.strip) instance.send("#{name}_content_type=", file.content_type.strip)
instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size") instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
instance.save(false) if Rails.version >= "3.0.0"
instance.save(:validate => false)
else
instance.save(false)
end
else else
true true
end end
end end
end end
end end
desc "Regenerates missing thumbnail styles for all classes using Paperclip."
task :missing_styles => :environment do
# Force loading all model classes to never miss any has_attached_file declaration:
Dir[Rails.root + 'app/models/**/*.rb'].each { |path| load path }
Paperclip.missing_attachments_styles.each do |klass, attachment_definitions|
attachment_definitions.each do |attachment_name, missing_styles|
puts "Regenerating #{klass} -> #{attachment_name} -> #{missing_styles.inspect}"
ENV['CLASS'] = klass.to_s
ENV['ATTACHMENT'] = attachment_name.to_s
ENV['STYLES'] = missing_styles.join(',')
Rake::Task['paperclip:refresh:thumbnails'].execute
end
end
Paperclip.save_current_attachments_styles!
end
end end
desc "Cleans out invalid attachments. Useful after you've added new validations." desc "Cleans out invalid attachments. Useful after you've added new validations."
task :clean => :environment do task :clean => :environment do
klass = obtain_class klass = Paperclip::Task.obtain_class
names = obtain_attachments(klass) names = Paperclip::Task.obtain_attachments(klass)
names.each do |name| names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance| Paperclip.each_instance_with_attachment(klass, name) do |instance|
instance.send(name).send(:validate) unless instance.valid?
if instance.send(name).valid? attributes = %w(file_size file_name content_type).map{ |suffix| "#{name}_#{suffix}".to_sym }
true if attributes.any?{ |attribute| instance.errors[attribute].present? }
else instance.send("#{name}=", nil)
instance.send("#{name}=", nil) if Rails.version >= "3.0.0"
instance.save instance.save(:validate => false)
else
instance.save(false)
end
end
end end
end end
end end
......
...@@ -13,7 +13,7 @@ spec = Gem::Specification.new do |s| ...@@ -13,7 +13,7 @@ spec = Gem::Specification.new do |s|
s.version = Paperclip::VERSION s.version = Paperclip::VERSION
s.author = "Jon Yurek" s.author = "Jon Yurek"
s.email = "jyurek@thoughtbot.com" s.email = "jyurek@thoughtbot.com"
s.homepage = "http://www.thoughtbot.com/projects/paperclip" s.homepage = "https://github.com/thoughtbot/paperclip"
s.description = "Easy upload management for ActiveRecord" s.description = "Easy upload management for ActiveRecord"
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = "File attachments as attributes for ActiveRecord" s.summary = "File attachments as attributes for ActiveRecord"
...@@ -21,15 +21,18 @@ spec = Gem::Specification.new do |s| ...@@ -21,15 +21,18 @@ spec = Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.test_files = Dir["test/**/test_*.rb"] s.test_files = Dir["test/**/test_*.rb"]
s.rubyforge_project = "paperclip" s.rubyforge_project = "paperclip"
s.has_rdoc = true
s.extra_rdoc_files = Dir["README*"] s.extra_rdoc_files = Dir["README*"]
s.rdoc_options << '--line-numbers' << '--inline-source' s.rdoc_options << '--line-numbers' << '--inline-source'
s.requirements << "ImageMagick" s.requirements << "ImageMagick"
s.add_dependency 'activerecord' s.add_dependency 'activerecord', '>=2.3.0'
s.add_dependency 'activesupport' s.add_dependency 'activesupport', '>=2.3.2'
s.add_dependency 'cocaine', '>=0.0.2'
s.add_dependency 'mime-types'
s.add_development_dependency 'shoulda' s.add_development_dependency 'shoulda'
s.add_development_dependency 'appraisal' s.add_development_dependency 'appraisal'
s.add_development_dependency 'mocha' s.add_development_dependency 'mocha'
s.add_development_dependency 'aws-s3' s.add_development_dependency 'aws-s3'
s.add_development_dependency 'sqlite3-ruby' s.add_development_dependency 'sqlite3'
s.add_development_dependency 'cucumber'
s.add_development_dependency 'capybara'
end end
...@@ -109,8 +109,14 @@ if defined?(ActionController::Integration::Session) ...@@ -109,8 +109,14 @@ if defined?(ActionController::Integration::Session)
end end
end end
class Factory if defined?(FactoryGirl::Factory)
include Paperclip::Shoulda #:nodoc: class FactoryGirl::Factory
include Paperclip::Shoulda #:nodoc:
end
else
class Factory
include Paperclip::Shoulda #:nodoc:
end
end end
class Test::Unit::TestCase #:nodoc: class Test::Unit::TestCase #:nodoc:
......
...@@ -14,6 +14,35 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -14,6 +14,35 @@ class AttachmentTest < Test::Unit::TestCase
assert_equal "#{Rails.root}/public/fake_models/1234/fake", @attachment.path assert_equal "#{Rails.root}/public/fake_models/1234/fake", @attachment.path
end end
should "return the url by interpolating the default_url option when no file assigned" do
@attachment = attachment :default_url => ":class/blegga.png"
@model = @attachment.instance
assert_nil @model.avatar_file_name
assert_equal "fake_models/blegga.png", @attachment.url
end
should "return the url by executing and interpolating the default_url Proc when no file assigned" do
@attachment = attachment :default_url => lambda { |a| ":class/blegga.png" }
@model = @attachment.instance
assert_nil @model.avatar_file_name
assert_equal "fake_models/blegga.png", @attachment.url
end
should "return the url by executing and interpolating the default_url Proc with attachment arg when no file assigned" do
@attachment = attachment :default_url => lambda { |a| a.instance.some_method_to_determine_default_url }
@model = @attachment.instance
@model.stubs(:some_method_to_determine_default_url).returns(":class/blegga.png")
assert_nil @model.avatar_file_name
assert_equal "fake_models/blegga.png", @attachment.url
end
should "return the url by executing and interpolating the default_url when assigned with symbol as method in attachment model" do
@attachment = attachment :default_url => :some_method_to_determine_default_url
@model = @attachment.instance
@model.stubs(:some_method_to_determine_default_url).returns(":class/female_:style_blegga.png")
assert_equal "fake_models/female_foostyle_blegga.png", @attachment.url(:foostyle)
end
context "Attachment default_options" do context "Attachment default_options" do
setup do setup do
rebuild_model rebuild_model
...@@ -56,7 +85,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -56,7 +85,7 @@ class AttachmentTest < Test::Unit::TestCase
Paperclip::Attachment.default_options.keys.each do |key| Paperclip::Attachment.default_options.keys.each do |key|
should "be the default_options for #{key}" do should "be the default_options for #{key}" do
assert_equal @old_default_options[key], assert_equal @old_default_options[key],
@attachment.instance_variable_get("@#{key}"), @attachment.options.send(key),
key key
end end
end end
...@@ -71,7 +100,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -71,7 +100,7 @@ class AttachmentTest < Test::Unit::TestCase
Paperclip::Attachment.default_options.keys.each do |key| Paperclip::Attachment.default_options.keys.each do |key|
should "be the new default_options for #{key}" do should "be the new default_options for #{key}" do
assert_equal @new_default_options[key], assert_equal @new_default_options[key],
@attachment.instance_variable_get("@#{key}"), @attachment.options.send(key),
key key
end end
end end
...@@ -155,12 +184,12 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -155,12 +184,12 @@ class AttachmentTest < Test::Unit::TestCase
end end
should "interpolate the hash data" do should "interpolate the hash data" do
@attachment.expects(:interpolate).with(@attachment.options[:hash_data],anything).returns("interpolated_stuff") @attachment.expects(:interpolate).with(@attachment.options.hash_data,anything).returns("interpolated_stuff")
@attachment.hash @attachment.hash
end end
should "result in the correct interpolation" do should "result in the correct interpolation" do
assert_equal "fake_models/avatars/1234/original/1234567890", @attachment.send(:interpolate,@attachment.options[:hash_data]) assert_equal "fake_models/avatars/1234/original/1234567890", @attachment.send(:interpolate,@attachment.options.hash_data)
end end
should "result in a correct hash" do should "result in a correct hash" do
...@@ -228,6 +257,47 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -228,6 +257,47 @@ class AttachmentTest < Test::Unit::TestCase
end end
end end
context "An attachment with :source_file_options" do
setup do
rebuild_model :styles => {
:thumb => "100x100",
:large => "400x400"
},
:source_file_options => {
:all => "-density 400",
:thumb => "-depth 8"
}
@dummy = Dummy.new
@dummy.avatar
end
should "report the correct options when sent #extra_source_file_options_for(:thumb)" do
assert_equal "-depth 8 -density 400", @dummy.avatar.send(:extra_source_file_options_for, :thumb), @dummy.avatar.options.source_file_options.inspect
end
should "report the correct options when sent #extra_source_file_options_for(:large)" do
assert_equal "-density 400", @dummy.avatar.send(:extra_source_file_options_for, :large)
end
end
context "An attachment with :only_process" do
setup do
rebuild_model :styles => {
:thumb => "100x100",
:large => "400x400"
},
:only_process => [:thumb]
@file = StringIO.new("...")
@attachment = Dummy.new.avatar
end
should "only process the provided style" do
@attachment.expects(:post_process).with(:thumb)
@attachment.expects(:post_process).with(:large).never
@attachment.assign(@file)
end
end
context "An attachment with :convert_options that is a proc" do context "An attachment with :convert_options that is a proc" do
setup do setup do
rebuild_model :styles => { rebuild_model :styles => {
...@@ -284,7 +354,25 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -284,7 +354,25 @@ class AttachmentTest < Test::Unit::TestCase
end end
should "have the correct geometry" do should "have the correct geometry" do
assert_equal "50x50#", @attachment.styles[:thumb][:geometry] assert_equal "50x50#", @attachment.options.styles[:thumb][:geometry]
end
end
context "An attachment with conditional :styles that is a proc" do
setup do
rebuild_model :styles => lambda{ |attachment| attachment.instance.other == 'a' ? {:thumb => "50x50#"} : {:large => "400x400"} }
@dummy = Dummy.new(:other => 'a')
end
should "have the correct styles for the assigned instance values" do
assert_equal "50x50#", @dummy.avatar.options.styles[:thumb][:geometry]
assert_nil @dummy.avatar.options.styles[:large]
@dummy.other = 'b'
assert_equal "400x400", @dummy.avatar.options.styles[:large][:geometry]
assert_nil @dummy.avatar.options.styles[:thumb]
end end
end end
...@@ -328,7 +416,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -328,7 +416,7 @@ class AttachmentTest < Test::Unit::TestCase
end end
should "have the correct geometry" do should "have the correct geometry" do
assert_equal "50x50#", @attachment.styles[:normal][:geometry] assert_equal "50x50#", @attachment.options.styles[:normal][:geometry]
end end
end end
end end
...@@ -346,13 +434,17 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -346,13 +434,17 @@ class AttachmentTest < Test::Unit::TestCase
[:processors, :whiny, :convert_options, :geometry, :format].each do |field| [:processors, :whiny, :convert_options, :geometry, :format].each do |field|
should "have the same #{field} field" do should "have the same #{field} field" do
assert_equal @attachment.styles[:normal][field], @attachment.styles[:hash][field] assert_equal @attachment.options.styles[:normal][field], @attachment.options.styles[:hash][field]
end end
end end
end end
context "An attachment with :processors that is a proc" do context "An attachment with :processors that is a proc" do
setup do setup do
class Paperclip::Test < Paperclip::Processor; end
@file = StringIO.new("...")
Paperclip::Test.stubs(:make).returns(@file)
rebuild_model :styles => { :normal => '' }, :processors => lambda { |a| [ :test ] } rebuild_model :styles => { :normal => '' }, :processors => lambda { |a| [ :test ] }
@attachment = Dummy.new.avatar @attachment = Dummy.new.avatar
end end
...@@ -363,7 +455,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -363,7 +455,7 @@ class AttachmentTest < Test::Unit::TestCase
end end
should "have the correct processors" do should "have the correct processors" do
assert_equal [ :test ], @attachment.styles[:normal][:processors] assert_equal [ :test ], @attachment.options.styles[:normal][:processors]
end end
end end
end end
...@@ -405,7 +497,12 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -405,7 +497,12 @@ class AttachmentTest < Test::Unit::TestCase
end end
before_should "call #make with the right parameters passed as second argument" do before_should "call #make with the right parameters passed as second argument" do
expected_params = @style_params[:once].merge({:processors => [:thumbnail, :test], :whiny => true, :convert_options => ""}) expected_params = @style_params[:once].merge({
:processors => [:thumbnail, :test],
:whiny => true,
:convert_options => "",
:source_file_options => ""
})
Paperclip::Thumbnail.expects(:make).with(anything, expected_params, anything).returns(@file) Paperclip::Thumbnail.expects(:make).with(anything, expected_params, anything).returns(@file)
end end
...@@ -425,6 +522,19 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -425,6 +522,19 @@ class AttachmentTest < Test::Unit::TestCase
rebuild_model :storage => :FileSystem rebuild_model :storage => :FileSystem
@dummy = Dummy.new @dummy = Dummy.new
assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem) assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem)
rebuild_model :storage => :Filesystem
@dummy = Dummy.new
assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem)
end
should "convert underscored storage name to camelcase" do
rebuild_model :storage => :not_here
@dummy = Dummy.new
exception = assert_raises(Paperclip::StorageMethodNotFound) do |e|
@dummy.avatar
end
assert exception.message.include?("NotHere")
end end
should "raise an error if you try to include a storage module that doesn't exist" do should "raise an error if you try to include a storage module that doesn't exist" do
...@@ -642,6 +752,15 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -642,6 +752,15 @@ class AttachmentTest < Test::Unit::TestCase
assert_equal nil, @attachment.path(:blah) assert_equal nil, @attachment.path(:blah)
end end
context "with a file assigned but not saved yet" do
should "clear out any attached files" do
@attachment.assign(@file)
assert !@attachment.queued_for_write.blank?
@attachment.clear
assert @attachment.queued_for_write.blank?
end
end
context "with a file assigned in the database" do context "with a file assigned in the database" do
setup do setup do
@attachment.stubs(:instance_read).with(:file_name).returns("5k.png") @attachment.stubs(:instance_read).with(:file_name).returns("5k.png")
...@@ -749,7 +868,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -749,7 +868,7 @@ class AttachmentTest < Test::Unit::TestCase
context "and trying to delete" do context "and trying to delete" do
setup do setup do
@existing_names = @attachment.styles.keys.collect do |style| @existing_names = @attachment.options.styles.keys.collect do |style|
@attachment.path(style) @attachment.path(style)
end end
end end
...@@ -786,7 +905,22 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -786,7 +905,22 @@ class AttachmentTest < Test::Unit::TestCase
end end
end end
end end
end
context "with a file that has space in file name" do
setup do
@attachment.stubs(:instance_read).with(:file_name).returns("spaced file.png")
@attachment.stubs(:instance_read).with(:content_type).returns("image/png")
@attachment.stubs(:instance_read).with(:file_size).returns(12345)
dtnow = DateTime.now
@now = Time.now
Time.stubs(:now).returns(@now)
@attachment.stubs(:instance_read).with(:updated_at).returns(dtnow)
end
should "returns an escaped version of the URL" do
assert_match /\/spaced%20file\.png/, @attachment.url
end
end end
context "when trying a nonexistant storage type" do context "when trying a nonexistant storage type" do
...@@ -918,4 +1052,69 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -918,4 +1052,69 @@ class AttachmentTest < Test::Unit::TestCase
end end
end end
end end
context "an attachment with delete_file option set to false" do
setup do
rebuild_model :preserve_files => true
@dummy = Dummy.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
@dummy.avatar = @file
@dummy.save!
@attachment = @dummy.avatar
@path = @attachment.path
end
should "not delete the files from storage when attachment is destroyed" do
@attachment.destroy
assert File.exists?(@path)
end
should "not delete the file when model is destroyed" do
@dummy.destroy
assert File.exists?(@path)
end
end
context "setting an interpolation class" do
should "produce the URL with the given interpolations" do
Interpolator = Class.new do
def self.interpolate(pattern, attachment, style_name)
"hello"
end
end
instance = Dummy.new
attachment = Paperclip::Attachment.new(:avatar, instance, :interpolator => Interpolator)
assert_equal "hello", attachment.url
end
end
context "An attached file" do
setup do
rebuild_model
@dummy = Dummy.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
@dummy.avatar = @file
@dummy.save!
@attachment = @dummy.avatar
@path = @attachment.path
end
should "not be deleted when the model fails to destroy" do
@dummy.stubs(:destroy).raises(Exception)
assert_raise Exception do
@dummy.destroy
end
assert File.exists?(@path), "#{@path} does not exist."
end
should "be deleted when the model is destroyed" do
@dummy.destroy
assert ! File.exists?(@path), "#{@path} does not exist."
end
end
end end
require './test/helper'
class CommandLineTest < Test::Unit::TestCase
def setup
Paperclip::CommandLine.path = nil
File.stubs(:exist?).with("/dev/null").returns(true)
end
should "allow colons in parameters" do
cmd = Paperclip::CommandLine.new("convert", "'a.jpg' -resize 175x220> -size 175x220 xc:black +swap -gravity center -composite 'b.jpg'", :swallow_stderr => false)
assert_equal "convert 'a.jpg' -resize 175x220> -size 175x220 xc:black +swap -gravity center -composite 'b.jpg'", cmd.command
end
should "take a command and parameters and produce a shell command for bash" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
assert_equal "convert a.jpg b.png", cmd.command
end
should "be able to set a path and produce commands with that path" do
Paperclip::CommandLine.path = "/opt/bin"
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
assert_equal "/opt/bin/convert a.jpg b.png", cmd.command
end
should "be able to interpolate quoted variables into the parameters" do
cmd = Paperclip::CommandLine.new("convert",
":one :{two}",
:one => "a.jpg",
:two => "b.png",
:swallow_stderr => false)
assert_equal "convert 'a.jpg' 'b.png'", cmd.command
end
should "quote command line options differently if we're on windows" do
File.stubs(:exist?).with("/dev/null").returns(false)
cmd = Paperclip::CommandLine.new("convert",
":one :{two}",
:one => "a.jpg",
:two => "b.png",
:swallow_stderr => false)
assert_equal 'convert "a.jpg" "b.png"', cmd.command
end
should "be able to quote and interpolate dangerous variables" do
cmd = Paperclip::CommandLine.new("convert",
":one :two",
:one => "`rm -rf`.jpg",
:two => "ha'ha.png",
:swallow_stderr => false)
assert_equal "convert '`rm -rf`.jpg' 'ha'\\''ha.png'", cmd.command
end
should "be able to quote and interpolate dangerous variables even on windows" do
File.stubs(:exist?).with("/dev/null").returns(false)
cmd = Paperclip::CommandLine.new("convert",
":one :two",
:one => "`rm -rf`.jpg",
:two => "ha'ha.png",
:swallow_stderr => false)
assert_equal %{convert "`rm -rf`.jpg" "ha'ha.png"}, cmd.command
end
should "add redirection to get rid of stderr in bash" do
File.stubs(:exist?).with("/dev/null").returns(true)
cmd = Paperclip::CommandLine.new("convert",
"a.jpg b.png",
:swallow_stderr => true)
assert_equal "convert a.jpg b.png 2>/dev/null", cmd.command
end
should "add redirection to get rid of stderr in cmd.exe" do
File.stubs(:exist?).with("/dev/null").returns(false)
cmd = Paperclip::CommandLine.new("convert",
"a.jpg b.png",
:swallow_stderr => true)
assert_equal "convert a.jpg b.png 2>NUL", cmd.command
end
should "raise if trying to interpolate :swallow_stderr or :expected_outcodes" do
cmd = Paperclip::CommandLine.new("convert",
":swallow_stderr :expected_outcodes",
:swallow_stderr => false,
:expected_outcodes => [0, 1])
assert_raise(Paperclip::PaperclipCommandLineError) do
cmd.command
end
end
should "run the #command it's given and return the output" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(0) do
assert_equal :correct_value, cmd.run
end
end
should "raise a PaperclipCommandLineError if the result code isn't expected" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(1) do
assert_raises(Paperclip::PaperclipCommandLineError) do
cmd.run
end
end
end
should "not raise a PaperclipCommandLineError if the result code is expected" do
cmd = Paperclip::CommandLine.new("convert",
"a.jpg b.png",
:expected_outcodes => [0, 1],
:swallow_stderr => false)
cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(1) do
assert_nothing_raised do
cmd.run
end
end
end
should "log the command" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.class.stubs(:'`')
Paperclip.expects(:log).with("convert a.jpg b.png")
cmd.run
end
should "detect that the system is unix or windows based on presence of /dev/null" do
File.stubs(:exist?).returns(true)
assert Paperclip::CommandLine.unix?
end
should "detect that the system is not unix or windows based on absence of /dev/null" do
File.stubs(:exist?).returns(false)
assert ! Paperclip::CommandLine.unix?
end
end
development:
provider: AWS
aws_access_key_id: AWS_ID
aws_secret_access_key: AWS_SECRET
test:
provider: AWS
aws_access_key_id: AWS_ID
aws_secret_access_key: AWS_SECRET
require './test/helper'
require 'fog'
Fog.mock!
class FogTest < Test::Unit::TestCase
context "" do
context "with credentials provided in a path string" do
setup do
rebuild_model :styles => { :medium => "300x300>", :thumb => "100x100>" },
:storage => :fog,
:url => '/:attachment/:filename',
:fog_directory => "paperclip",
:fog_credentials => File.join(File.dirname(__FILE__), 'fixtures', 'fog.yml')
@dummy = Dummy.new
@dummy.avatar = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb')
end
should "have the proper information loading credentials from a file" do
assert_equal @dummy.avatar.fog_credentials[:provider], 'AWS'
end
end
context "with credentials provided in a File object" do
setup do
rebuild_model :styles => { :medium => "300x300>", :thumb => "100x100>" },
:storage => :fog,
:url => '/:attachment/:filename',
:fog_directory => "paperclip",
:fog_credentials => File.open(File.join(File.dirname(__FILE__), 'fixtures', 'fog.yml'))
@dummy = Dummy.new
@dummy.avatar = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb')
end
should "have the proper information loading credentials from a file" do
assert_equal @dummy.avatar.fog_credentials[:provider], 'AWS'
end
end
context "with default values for path and url" do
setup do
rebuild_model :styles => { :medium => "300x300>", :thumb => "100x100>" },
:storage => :fog,
:url => '/:attachment/:filename',
:fog_directory => "paperclip",
:fog_credentials => {
:provider => 'AWS',
:aws_access_key_id => 'AWS_ID',
:aws_secret_access_key => 'AWS_SECRET'
}
@dummy = Dummy.new
@dummy.avatar = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb')
end
should "be able to interpolate the path without blowing up" do
assert_equal File.expand_path(File.join(File.dirname(__FILE__), "../public/avatars/5k.png")),
@dummy.avatar.path
end
should "clean up file objects" do
File.stubs(:exist?).returns(true)
Paperclip::Tempfile.any_instance.expects(:close).at_least_once()
Paperclip::Tempfile.any_instance.expects(:unlink).at_least_once()
@dummy.save!
end
end
setup do
@fog_directory = 'papercliptests'
@credentials = {
:provider => 'AWS',
:aws_access_key_id => 'ID',
:aws_secret_access_key => 'SECRET'
}
@connection = Fog::Storage.new(@credentials)
@connection.directories.create(
:key => @fog_directory
)
@options = {
:fog_directory => @fog_directory,
:fog_credentials => @credentials,
:fog_host => nil,
:fog_file => {:cache_control => 1234},
:path => ":attachment/:basename.:extension",
:storage => :fog
}
rebuild_model(@options)
end
should "be extended by the Fog module" do
assert Dummy.new.avatar.is_a?(Paperclip::Storage::Fog)
end
context "when assigned" do
setup do
@file = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
teardown do
@file.close
directory = @connection.directories.new(:key => @fog_directory)
directory.files.each {|file| file.destroy}
directory.destroy
end
context "without a bucket" do
setup do
@connection.directories.get(@fog_directory).destroy
end
should "create the bucket" do
assert @dummy.save
assert @connection.directories.get(@fog_directory)
end
end
context "with a bucket" do
should "succeed" do
assert @dummy.save
end
end
context "without a fog_host" do
setup do
rebuild_model(@options.merge(:fog_host => nil))
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
should "provide a public url" do
assert !@dummy.avatar.url.nil?
end
end
context "with a fog_host" do
setup do
rebuild_model(@options.merge(:fog_host => 'http://example.com'))
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
should "provide a public url" do
assert @dummy.avatar.url =~ /^http:\/\/example\.com\/avatars\/stringio\.txt\?\d*$/
end
end
context "with a fog_host that includes a wildcard placeholder" do
setup do
rebuild_model(
:fog_directory => @fog_directory,
:fog_credentials => @credentials,
:fog_host => 'http://img%d.example.com',
:path => ":attachment/:basename.:extension",
:storage => :fog
)
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
should "provide a public url" do
assert @dummy.avatar.url =~ /^http:\/\/img[0123]\.example\.com\/avatars\/stringio\.txt\?\d*$/
end
end
context "with fog_public set to false" do
setup do
rebuild_model(@options.merge(:fog_public => false))
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
should 'set the @fog_public instance variable to false' do
assert_equal false, @dummy.avatar.options.fog_public
end
end
end
end
end
...@@ -120,6 +120,35 @@ class GeometryTest < Test::Unit::TestCase ...@@ -120,6 +120,35 @@ class GeometryTest < Test::Unit::TestCase
assert_raise(Paperclip::NotIdentifiedByImageMagickError){ @geo = Paperclip::Geometry.from_file(file) } assert_raise(Paperclip::NotIdentifiedByImageMagickError){ @geo = Paperclip::Geometry.from_file(file) }
end end
should "not generate from a blank filename" do
file = ""
assert_raise(Paperclip::NotIdentifiedByImageMagickError){ @geo = Paperclip::Geometry.from_file(file) }
end
should "not generate from a nil file" do
file = nil
assert_raise(Paperclip::NotIdentifiedByImageMagickError){ @geo = Paperclip::Geometry.from_file(file) }
end
should "not generate from a file with no path" do
file = mock("file", :path => "")
file.stubs(:respond_to?).with(:path).returns(true)
assert_raise(Paperclip::NotIdentifiedByImageMagickError){ @geo = Paperclip::Geometry.from_file(file) }
end
should "let us know when a command isn't found versus a processing error" do
old_path = ENV['PATH']
begin
ENV['PATH'] = ''
assert_raises(Paperclip::CommandNotFoundError) do
file = File.join(File.dirname(__FILE__), "fixtures", "5k.png")
@geo = Paperclip::Geometry.from_file(file)
end
ensure
ENV['PATH'] = old_path
end
end
[['vertical', 900, 1440, true, false, false, 1440, 900, 0.625], [['vertical', 900, 1440, true, false, false, 1440, 900, 0.625],
['horizontal', 1024, 768, false, true, false, 1024, 768, 1.3333], ['horizontal', 1024, 768, false, true, false, 1024, 768, 1.3333],
['square', 100, 100, false, false, true, 100, 100, 1]].each do |args| ['square', 100, 100, false, false, true, 100, 100, 1]].each do |args|
......
...@@ -8,6 +8,8 @@ require 'mocha' ...@@ -8,6 +8,8 @@ require 'mocha'
require 'active_record' require 'active_record'
require 'active_record/version' require 'active_record/version'
require 'active_support' require 'active_support'
require 'mime/types'
require 'pry'
puts "Testing against version #{ActiveRecord::VERSION::STRING}" puts "Testing against version #{ActiveRecord::VERSION::STRING}"
...@@ -19,7 +21,7 @@ rescue LoadError => e ...@@ -19,7 +21,7 @@ rescue LoadError => e
puts "debugger disabled" puts "debugger disabled"
end end
ROOT = File.join(File.dirname(__FILE__), '..') ROOT = Pathname(File.expand_path(File.join(File.dirname(__FILE__), '..')))
def silence_warnings def silence_warnings
old_verbose, $VERBOSE = $VERBOSE, nil old_verbose, $VERBOSE = $VERBOSE, nil
...@@ -47,6 +49,7 @@ FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures") ...@@ -47,6 +49,7 @@ FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/debug.log") ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config['test']) ActiveRecord::Base.establish_connection(config['test'])
Paperclip.options[:logger] = ActiveRecord::Base.logger
def reset_class class_name def reset_class class_name
ActiveRecord::Base.send(:include, Paperclip::Glue) ActiveRecord::Base.send(:include, Paperclip::Glue)
...@@ -67,6 +70,7 @@ end ...@@ -67,6 +70,7 @@ end
def rebuild_model options = {} def rebuild_model options = {}
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table| ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
table.column :title, :string
table.column :other, :string table.column :other, :string
table.column :avatar_file_name, :string table.column :avatar_file_name, :string
table.column :avatar_content_type, :string table.column :avatar_content_type, :string
...@@ -81,10 +85,12 @@ def rebuild_class options = {} ...@@ -81,10 +85,12 @@ def rebuild_class options = {}
ActiveRecord::Base.send(:include, Paperclip::Glue) ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, "Dummy") rescue nil Object.send(:remove_const, "Dummy") rescue nil
Object.const_set("Dummy", Class.new(ActiveRecord::Base)) Object.const_set("Dummy", Class.new(ActiveRecord::Base))
Paperclip.reset_duplicate_clash_check!
Dummy.class_eval do Dummy.class_eval do
include Paperclip::Glue include Paperclip::Glue
has_attached_file :avatar, options has_attached_file :avatar, options
end end
Dummy.reset_column_information
end end
class FakeModel class FakeModel
......
...@@ -34,20 +34,20 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -34,20 +34,20 @@ class IntegrationTest < Test::Unit::TestCase
should "create its thumbnails properly" do should "create its thumbnails properly" do
assert_match /\b50x50\b/, `identify "#{@dummy.avatar.path(:thumb)}"` assert_match /\b50x50\b/, `identify "#{@dummy.avatar.path(:thumb)}"`
end end
context 'reprocessing with unreadable original' do context 'reprocessing with unreadable original' do
setup { File.chmod(0000, @dummy.avatar.path) } setup { File.chmod(0000, @dummy.avatar.path) }
should "not raise an error" do should "not raise an error" do
assert_nothing_raised do assert_nothing_raised do
@dummy.avatar.reprocess! @dummy.avatar.reprocess!
end end
end end
should "return false" do should "return false" do
assert ! @dummy.avatar.reprocess! assert ! @dummy.avatar.reprocess!
end end
teardown { File.chmod(0644, @dummy.avatar.path) } teardown { File.chmod(0644, @dummy.avatar.path) }
end end
...@@ -58,6 +58,7 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -58,6 +58,7 @@ class IntegrationTest < Test::Unit::TestCase
has_attached_file :avatar, :styles => { :thumb => "150x25#", :dynamic => lambda { |a| '50x50#' } } has_attached_file :avatar, :styles => { :thumb => "150x25#", :dynamic => lambda { |a| '50x50#' } }
end end
@d2 = Dummy.find(@dummy.id) @d2 = Dummy.find(@dummy.id)
@original_timestamp = @d2.avatar_updated_at
@d2.avatar.reprocess! @d2.avatar.reprocess!
@d2.save @d2.save
end end
...@@ -66,6 +67,26 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -66,6 +67,26 @@ class IntegrationTest < Test::Unit::TestCase
assert_match /\b150x25\b/, `identify "#{@dummy.avatar.path(:thumb)}"` assert_match /\b150x25\b/, `identify "#{@dummy.avatar.path(:thumb)}"`
assert_match /\b50x50\b/, `identify "#{@dummy.avatar.path(:dynamic)}"` assert_match /\b50x50\b/, `identify "#{@dummy.avatar.path(:dynamic)}"`
end end
should "change the timestamp" do
assert_not_equal @original_timestamp, @d2.avatar_updated_at
end
should "clean up the old original if it is a tempfile" do
original = @d2.avatar.to_file(:original)
tf = Paperclip::Tempfile.new('original')
tf.binmode
original.binmode
tf.write(original.read)
original.close
tf.rewind
File.expects(:unlink).with(tf.instance_variable_get(:@tmpname))
@d2.avatar.expects(:to_file).with(:original).returns(tf)
@d2.avatar.reprocess!
end
end end
end end
...@@ -278,6 +299,38 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -278,6 +299,38 @@ class IntegrationTest < Test::Unit::TestCase
end end
end end
context "A model with no source_file_options setting" do
setup do
rebuild_model :styles => { :large => "300x300>",
:medium => "100x100",
:thumb => ["32x32#", :gif] },
:default_style => :medium,
:url => "/:attachment/:class/:style/:id/:basename.:extension",
:path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
@dummy = Dummy.new
end
should "have its definition return nil when asked about source_file_options" do
assert ! Dummy.attachment_definitions[:avatar][:source_file_options]
end
context "redefined to have source_file_options setting" do
setup do
rebuild_model :styles => { :large => "300x300>",
:medium => "100x100",
:thumb => ["32x32#", :gif] },
:source_file_options => "-density 400",
:default_style => :medium,
:url => "/:attachment/:class/:style/:id/:basename.:extension",
:path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
end
should "have its definition return source_file_options value when asked about source_file_options" do
assert_equal "-density 400", Dummy.attachment_definitions[:avatar][:source_file_options]
end
end
end
context "A model with a filesystem attachment" do context "A model with a filesystem attachment" do
setup do setup do
rebuild_model :styles => { :large => "300x300>", rebuild_model :styles => { :large => "300x300>",
...@@ -383,6 +436,24 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -383,6 +436,24 @@ class IntegrationTest < Test::Unit::TestCase
assert_equal "5k.png", @dummy.avatar_file_name assert_equal "5k.png", @dummy.avatar_file_name
end end
[000,002,022].each do |umask|
context "when the umask is #{umask}" do
setup do
@umask = File.umask umask
end
teardown do
File.umask @umask
end
should "respect the current umask" do
@dummy.avatar = @file
@dummy.save
assert_equal 0666&~umask, 0666&File.stat(@dummy.avatar.path).mode
end
end
end
context "that is assigned its file from another Paperclip attachment" do context "that is assigned its file from another Paperclip attachment" do
setup do setup do
@dummy2 = Dummy.new @dummy2 = Dummy.new
...@@ -399,6 +470,7 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -399,6 +470,7 @@ class IntegrationTest < Test::Unit::TestCase
@dummy.save @dummy.save
assert_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`, assert_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`,
`identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"` `identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"`
assert_equal @dummy.avatar_file_name, @dummy2.avatar_file_name
end end
end end
...@@ -466,6 +538,18 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -466,6 +538,18 @@ class IntegrationTest < Test::Unit::TestCase
@files_on_s3 = s3_files_for @dummy.avatar @files_on_s3 = s3_files_for @dummy.avatar
end end
context 'assigning itself to a new model' do
setup do
@d2 = Dummy.new
@d2.avatar = @dummy.avatar
@d2.save
end
should "have the same name as the old file" do
assert_equal @d2.avatar.original_filename, @dummy.avatar.original_filename
end
end
should "have the same contents as the original" do should "have the same contents as the original" do
@file.rewind @file.rewind
assert_equal @file.read, @files_on_s3[:original].read assert_equal @file.read, @files_on_s3[:original].read
......
require './test/helper'
class InterpolatedStringTest < Test::Unit::TestCase
context "inheritance" do
should "inherited from String" do
assert Paperclip::InterpolatedString.new("paperclip").is_a? String
end
end
context "#escape" do
subject { Paperclip::InterpolatedString.new("paperclip foo").escape }
should "returns an InterpolatedString object" do
assert subject.is_a? Paperclip::InterpolatedString
end
should "escape the output string" do
assert_equal "paperclip%20foo", subject
end
should "not double escape output string" do
assert_equal "paperclip%20foo", subject.escape
end
end
context "#unescape" do
subject { Paperclip::InterpolatedString.new("paperclip%20foo").escape.unescape }
should "returns an InterpolatedString object" do
assert subject.is_a? Paperclip::InterpolatedString
end
should "unescape the output string" do
assert_equal "paperclip%20foo", subject
end
should "not double unescape output string" do
assert_equal "paperclip%20foo", subject.unescape
end
end
context "#escaped?" do
subject { Paperclip::InterpolatedString.new("paperclip") }
should "returns true if string was escaped" do
assert subject.escape.escaped?
end
should "returns false if string wasn't escaped" do
assert !subject.escaped?
end
end
context "#force_escape" do
subject { Paperclip::InterpolatedString.new("paperclip") }
setup { subject.force_escape }
should "sets escaped flag to true" do
assert subject.escaped?
end
end
end
...@@ -50,6 +50,37 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -50,6 +50,37 @@ class InterpolationsTest < Test::Unit::TestCase
assert_equal "png", Paperclip::Interpolations.extension(attachment, :style) assert_equal "png", Paperclip::Interpolations.extension(attachment, :style)
end end
should "return the extension of the file based on the content type" do
attachment = mock
attachment.expects(:content_type).returns('image/jpeg')
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('random')
assert_equal "jpeg", interpolations.content_type_extension(attachment, :style)
end
should "return the original extension of the file if it matches a content type extension" do
attachment = mock
attachment.expects(:content_type).returns('image/jpeg')
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('jpe')
assert_equal "jpe", interpolations.content_type_extension(attachment, :style)
end
should "return the latter half of the content type of the extension if no match found" do
attachment = mock
attachment.expects(:content_type).at_least_once().returns('not/found')
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('random')
assert_equal "found", interpolations.content_type_extension(attachment, :style)
end
should "return the #to_param of the attachment" do
attachment = mock
attachment.expects(:to_param).returns("23-awesome")
attachment.expects(:instance).returns(attachment)
assert_equal "23-awesome", Paperclip::Interpolations.param(attachment, :style)
end
should "return the id of the attachment" do should "return the id of the attachment" do
attachment = mock attachment = mock
attachment.expects(:id).returns(23) attachment.expects(:id).returns(23)
...@@ -57,13 +88,20 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -57,13 +88,20 @@ class InterpolationsTest < Test::Unit::TestCase
assert_equal 23, Paperclip::Interpolations.id(attachment, :style) assert_equal 23, Paperclip::Interpolations.id(attachment, :style)
end end
should "return the partitioned id of the attachment" do should "return the partitioned id of the attachment when the id is an integer" do
attachment = mock attachment = mock
attachment.expects(:id).returns(23) attachment.expects(:id).returns(23)
attachment.expects(:instance).returns(attachment) attachment.expects(:instance).returns(attachment)
assert_equal "000/000/023", Paperclip::Interpolations.id_partition(attachment, :style) assert_equal "000/000/023", Paperclip::Interpolations.id_partition(attachment, :style)
end end
should "return the partitioned id of the attachment when the id is a string" do
attachment = mock
attachment.expects(:id).returns("32fnj23oio2f")
attachment.expects(:instance).returns(attachment)
assert_equal "32f/nj2/3oi", Paperclip::Interpolations.id_partition(attachment, :style)
end
should "return the name of the attachment" do should "return the name of the attachment" do
attachment = mock attachment = mock
attachment.expects(:name).returns("file") attachment.expects(:name).returns("file")
...@@ -110,6 +148,20 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -110,6 +148,20 @@ class InterpolationsTest < Test::Unit::TestCase
assert_equal "one.png", Paperclip::Interpolations.filename(attachment, :style) assert_equal "one.png", Paperclip::Interpolations.filename(attachment, :style)
end end
should "return the filename as basename when extension is blank" do
attachment = mock
attachment.stubs(:styles).returns({})
attachment.stubs(:original_filename).returns("one")
assert_equal "one", Paperclip::Interpolations.filename(attachment, :style)
end
should "return the basename when the extension contains regexp special characters" do
attachment = mock
attachment.stubs(:styles).returns({})
attachment.stubs(:original_filename).returns("one.ab)")
assert_equal "one", Paperclip::Interpolations.basename(attachment, :style)
end
should "return the timestamp" do should "return the timestamp" do
now = Time.now now = Time.now
zone = 'UTC' zone = 'UTC'
...@@ -126,13 +178,20 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -126,13 +178,20 @@ class InterpolationsTest < Test::Unit::TestCase
assert_equal seconds_since_epoch, Paperclip::Interpolations.updated_at(attachment, :style) assert_equal seconds_since_epoch, Paperclip::Interpolations.updated_at(attachment, :style)
end end
should "return hash" do should "return attachment's hash when passing both arguments" do
attachment = mock attachment = mock
fake_hash = "a_wicked_secure_hash" fake_hash = "a_wicked_secure_hash"
attachment.expects(:hash).returns(fake_hash) attachment.expects(:hash).returns(fake_hash)
assert_equal fake_hash, Paperclip::Interpolations.hash(attachment, :style) assert_equal fake_hash, Paperclip::Interpolations.hash(attachment, :style)
end end
should "return Object#hash when passing no argument" do
attachment = mock
fake_hash = "a_wicked_secure_hash"
attachment.expects(:hash).never.returns(fake_hash)
assert_not_equal fake_hash, Paperclip::Interpolations.hash
end
should "call all expected interpolations with the given arguments" do should "call all expected interpolations with the given arguments" do
Paperclip::Interpolations.expects(:id).with(:attachment, :style).returns(1234) Paperclip::Interpolations.expects(:id).with(:attachment, :style).returns(1234)
Paperclip::Interpolations.expects(:attachment).with(:attachment, :style).returns("attachments") Paperclip::Interpolations.expects(:attachment).with(:attachment, :style).returns("attachments")
......
...@@ -34,14 +34,54 @@ class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase ...@@ -34,14 +34,54 @@ class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase
should_accept_dummy_class should_accept_dummy_class
end end
context "given a class with other validations but matching types" do context "given a class with other validations but matching types" do
setup do setup do
@dummy_class.validates_presence_of :title @dummy_class.validates_presence_of :title
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*} @dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
end end
should_accept_dummy_class
end
context "given a class that matches and a matcher that only specifies 'allowing'" do
setup do
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
@matcher = self.class.validate_attachment_content_type(:avatar).
allowing(%w(image/png image/jpeg))
end
should_accept_dummy_class
end
context "given a class that does not match and a matcher that only specifies 'allowing'" do
setup do
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{audio/.*}
@matcher = self.class.validate_attachment_content_type(:avatar).
allowing(%w(image/png image/jpeg))
end
should_reject_dummy_class
end
context "given a class that matches and a matcher that only specifies 'rejecting'" do
setup do
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
@matcher = self.class.validate_attachment_content_type(:avatar).
rejecting(%w(audio/mp3 application/octet-stream))
end
should_accept_dummy_class should_accept_dummy_class
end end
context "given a class that does not match and a matcher that only specifies 'rejecting'" do
setup do
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{audio/.*}
@matcher = self.class.validate_attachment_content_type(:avatar).
rejecting(%w(audio/mp3 application/octet-stream))
end
should_reject_dummy_class
end
end end
end end
# encoding: utf-8
require './test/helper'
class MockAttachment < Struct.new(:one, :two)
def instance
self
end
end
class OptionsTest < Test::Unit::TestCase
should "be able to set a value" do
@options = Paperclip::Options.new(nil, {})
assert_nil @options.path
@options.path = "this/is/a/path"
assert_equal "this/is/a/path", @options.path
end
context "#styles with a plain hash" do
setup do
@attachment = MockAttachment.new(nil, nil)
@options = Paperclip::Options.new(@attachment,
:styles => {
:something => ["400x400", :png]
})
end
should "return the right data for the style's geometry" do
assert_equal "400x400", @options.styles[:something][:geometry]
end
should "return the right data for the style's format" do
assert_equal :png, @options.styles[:something][:format]
end
end
context "#styles is a proc" do
setup do
@attachment = MockAttachment.new("123x456", :doc)
@options = Paperclip::Options.new(@attachment,
:styles => lambda {|att|
{:something => {:geometry => att.one, :format => att.two}}
})
end
should "return the right data for the style's geometry" do
assert_equal "123x456", @options.styles[:something][:geometry]
end
should "return the right data for the style's format" do
assert_equal :doc, @options.styles[:something][:format]
end
should "run the proc each time, giving dynamic results" do
assert_equal :doc, @options.styles[:something][:format]
@attachment.two = :pdf
assert_equal :pdf, @options.styles[:something][:format]
end
end
context "#processors" do
setup do
@attachment = MockAttachment.new(nil, nil)
end
should "return processors if not a proc" do
@options = Paperclip::Options.new(@attachment, :processors => [:one])
assert_equal [:one], @options.processors
end
should "return processors if it is a proc" do
@options = Paperclip::Options.new(@attachment, :processors => lambda{|att| [att.one]})
assert_equal [nil], @options.processors
@attachment.one = :other
assert_equal [:other], @options.processors
end
end
end
require './test/helper'
class PaperclipMissingAttachmentStylesTest < Test::Unit::TestCase
context "Paperclip" do
setup do
Paperclip.classes_with_attachments = Set.new
end
teardown do
File.unlink(Paperclip.registered_attachments_styles_path) rescue nil
end
should "be able to keep list of models using it" do
assert_kind_of Set, Paperclip.classes_with_attachments
assert Paperclip.classes_with_attachments.empty?, 'list should be empty'
rebuild_model
assert_equal ['Dummy'].to_set, Paperclip.classes_with_attachments
end
should "enable to get and set path to registered styles file" do
assert_equal ROOT.join('public/system/paperclip_attachments.yml').to_s, Paperclip.registered_attachments_styles_path
Paperclip.registered_attachments_styles_path = '/tmp/config/paperclip_attachments.yml'
assert_equal '/tmp/config/paperclip_attachments.yml', Paperclip.registered_attachments_styles_path
Paperclip.registered_attachments_styles_path = nil
assert_equal ROOT.join('public/system/paperclip_attachments.yml').to_s, Paperclip.registered_attachments_styles_path
end
should "be able to get current attachment styles" do
assert_equal Hash.new, Paperclip.send(:current_attachments_styles)
rebuild_model :styles => {:croppable => '600x600>', :big => '1000x1000>'}
expected_hash = { :Dummy => {:avatar => [:big, :croppable]}}
assert_equal expected_hash, Paperclip.send(:current_attachments_styles)
end
should "be able to save current attachment styles for further comparison" do
rebuild_model :styles => {:croppable => '600x600>', :big => '1000x1000>'}
Paperclip.save_current_attachments_styles!
expected_hash = { :Dummy => {:avatar => [:big, :croppable]}}
assert_equal expected_hash, YAML.load_file(Paperclip.registered_attachments_styles_path)
end
should "be able to read registered attachment styles from file" do
rebuild_model :styles => {:croppable => '600x600>', :big => '1000x1000>'}
Paperclip.save_current_attachments_styles!
expected_hash = { :Dummy => {:avatar => [:big, :croppable]}}
assert_equal expected_hash, Paperclip.send(:get_registered_attachments_styles)
end
should "be able to calculate differences between registered styles and current styles" do
rebuild_model :styles => {:croppable => '600x600>', :big => '1000x1000>'}
Paperclip.save_current_attachments_styles!
rebuild_model :styles => {:thumb => 'x100', :export => 'x400>', :croppable => '600x600>', :big => '1000x1000>'}
expected_hash = { :Dummy => {:avatar => [:export, :thumb]} }
assert_equal expected_hash, Paperclip.missing_attachments_styles
ActiveRecord::Base.connection.create_table :books, :force => true
class ::Book < ActiveRecord::Base
has_attached_file :cover, :styles => {:small => 'x100', :large => '1000x1000>'}
has_attached_file :sample, :styles => {:thumb => 'x100'}
end
expected_hash = {
:Dummy => {:avatar => [:export, :thumb]},
:Book => {:sample => [:thumb], :cover => [:large, :small]}
}
assert_equal expected_hash, Paperclip.missing_attachments_styles
Paperclip.save_current_attachments_styles!
assert_equal Hash.new, Paperclip.missing_attachments_styles
end
# It's impossible to build styles hash without loading from database whole bunch of records
should "skip lambda-styles" do
rebuild_model :styles => lambda{ |attachment| attachment.instance.other == 'a' ? {:thumb => "50x50#"} : {:large => "400x400"} }
assert_equal Hash.new, Paperclip.send(:current_attachments_styles)
end
end
end
...@@ -3,43 +3,32 @@ require './test/helper' ...@@ -3,43 +3,32 @@ require './test/helper'
class PaperclipTest < Test::Unit::TestCase class PaperclipTest < Test::Unit::TestCase
context "Calling Paperclip.run" do context "Calling Paperclip.run" do
setup do setup do
Paperclip.options[:image_magick_path] = nil Paperclip.options[:log_command] = false
Paperclip.options[:command_path] = nil Cocaine::CommandLine.expects(:new).with("convert", "stuff", {}).returns(stub(:run))
Paperclip::CommandLine.stubs(:'`') @original_command_line_path = Cocaine::CommandLine.path
end end
should "execute the right command with :image_magick_path" do teardown do
Paperclip.options[:image_magick_path] = "/usr/bin" Paperclip.options[:log_command] = true
Paperclip.expects(:log).with(includes('[DEPRECATION]')) Cocaine::CommandLine.path = @original_command_line_path
Paperclip.expects(:log).with(regexp_matches(%r{/usr/bin/convert ['"]one.jpg['"] ['"]two.jpg['"]}))
Paperclip::CommandLine.expects(:"`").with(regexp_matches(%r{/usr/bin/convert ['"]one.jpg['"] ['"]two.jpg['"]}))
Paperclip.run("convert", ":one :two", :one => "one.jpg", :two => "two.jpg")
end end
should "execute the right command with :command_path" do should "run the command with Cocaine" do
Paperclip.options[:command_path] = "/usr/bin" Paperclip.run("convert", "stuff")
Paperclip::CommandLine.expects(:"`").with(regexp_matches(%r{/usr/bin/convert ['"]one.jpg['"] ['"]two.jpg['"]}))
Paperclip.run("convert", ":one :two", :one => "one.jpg", :two => "two.jpg")
end end
should "execute the right command with no path" do should "save Cocaine::CommandLine.path that set before" do
Paperclip::CommandLine.expects(:"`").with(regexp_matches(%r{convert ['"]one.jpg['"] ['"]two.jpg['"]})) Cocaine::CommandLine.path = "/opt/my_app/bin"
Paperclip.run("convert", ":one :two", :one => "one.jpg", :two => "two.jpg") Paperclip.run("convert", "stuff")
assert_equal [Cocaine::CommandLine.path].flatten.include?("/opt/my_app/bin"), true
end end
end
should "tell you the command isn't there if the shell returns 127" do context "Calling Paperclip.run with a logger" do
with_exitstatus_returning(127) do should "pass the defined logger if :log_command is set" do
assert_raises(Paperclip::CommandNotFoundError) do Paperclip.options[:log_command] = true
Paperclip.run("command") Cocaine::CommandLine.expects(:new).with("convert", "stuff", :logger => Paperclip.logger).returns(stub(:run))
end Paperclip.run("convert", "stuff")
end
end
should "tell you the command isn't there if an ENOENT is raised" do
assert_raises(Paperclip::CommandNotFoundError) do
Paperclip::CommandLine.stubs(:"`").raises(Errno::ENOENT)
Paperclip.run("command")
end
end end
end end
...@@ -60,10 +49,6 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -60,10 +49,6 @@ class PaperclipTest < Test::Unit::TestCase
end end
end end
should "raise when sent #processor and the name of a class that exists but isn't a subclass of Processor" do
assert_raises(Paperclip::PaperclipError){ Paperclip.processor(:attachment) }
end
should "raise when sent #processor and the name of a class that doesn't exist" do should "raise when sent #processor and the name of a class that doesn't exist" do
assert_raises(NameError){ Paperclip.processor(:boogey_man) } assert_raises(NameError){ Paperclip.processor(:boogey_man) }
end end
...@@ -72,6 +57,37 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -72,6 +57,37 @@ class PaperclipTest < Test::Unit::TestCase
assert_equal ::Paperclip::Thumbnail, Paperclip.processor(:thumbnail) assert_equal ::Paperclip::Thumbnail, Paperclip.processor(:thumbnail)
end end
should "get a class from a namespaced class name" do
class ::One; class Two; end; end
assert_equal ::One::Two, Paperclip.class_for("One::Two")
end
should "raise when class doesn't exist in specified namespace" do
class ::Three; end
class ::Four; end
assert_raise NameError do
Paperclip.class_for("Three::Four")
end
end
context "Attachments with clashing URLs should raise error" do
setup do
class Dummy2 < ActiveRecord::Base
include Paperclip::Glue
end
end
should "generate warning if attachment is redefined with the same url string" do
Paperclip.expects(:log).with("Duplicate URL for blah with /system/:attachment/:id/:style/:filename. This will clash with attachment defined in Dummy class")
Dummy.class_eval do
has_attached_file :blah
end
Dummy2.class_eval do
has_attached_file :blah
end
end
end
context "An ActiveRecord model with an 'avatar' attachment" do context "An ActiveRecord model with an 'avatar' attachment" do
setup do setup do
rebuild_model :path => "tmp/:class/omg/:style.:extension" rebuild_model :path => "tmp/:class/omg/:style.:extension"
...@@ -223,7 +239,7 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -223,7 +239,7 @@ class PaperclipTest < Test::Unit::TestCase
@dummy.valid? @dummy.valid?
end end
should "not have an error when assigned a valid file" do should "not have an error when assigned a valid file" do
assert_equal 0, @dummy.errors.length, @dummy.errors.full_messages.join(", ") assert_equal 0, @dummy.errors.size, @dummy.errors.full_messages.join(", ")
end end
end end
context "and assigned an invalid file" do context "and assigned an invalid file" do
...@@ -232,7 +248,7 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -232,7 +248,7 @@ class PaperclipTest < Test::Unit::TestCase
@dummy.valid? @dummy.valid?
end end
should "have an error when assigned a valid file" do should "have an error when assigned a valid file" do
assert @dummy.errors.length > 0 assert @dummy.errors.size > 0
end end
end end
end end
...@@ -298,4 +314,26 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -298,4 +314,26 @@ class PaperclipTest < Test::Unit::TestCase
end end
end end
context "configuring a custom processor" do
setup do
@freedom_processor = Class.new do
def make(file, options = {}, attachment = nil)
file
end
end.new
Paperclip.configure do |config|
config.register_processor(:freedom, @freedom_processor)
end
end
should "be able to find the custom processor" do
assert_equal @freedom_processor, Paperclip.processor(:freedom)
end
teardown do
Paperclip.clear_processors!
end
end
end end
require './test/helper'
class FileSystemTest < Test::Unit::TestCase
context "Filesystem" do
setup do
rebuild_model :styles => { :thumbnail => "25x25#" }
@dummy = Dummy.create!
@dummy.avatar = File.open(File.join(File.dirname(__FILE__), "..", "fixtures", "5k.png"))
end
should "allow file assignment" do
assert @dummy.save
end
should "store the original" do
@dummy.save
assert File.exists?(@dummy.avatar.path)
end
should "store the thumbnail" do
@dummy.save
assert File.exists?(@dummy.avatar.path(:thumbnail))
end
should "clean up file objects" do
File.stubs(:exist?).returns(true)
Paperclip::Tempfile.any_instance.expects(:close).at_least_once()
Paperclip::Tempfile.any_instance.expects(:unlink).at_least_once()
@dummy.save!
end
context "with file that has space in file name" do
setup do
rebuild_model :styles => { :thumbnail => "25x25#" }
@dummy = Dummy.create!
@dummy.avatar = File.open(File.join(File.dirname(__FILE__), "..", "fixtures", "spaced file.png"))
@dummy.save
end
should "store the file" do
assert File.exists?(@dummy.avatar.path)
end
should "store the path unescaped" do
assert_match /\/spaced file\.png/, @dummy.avatar.path
end
should "return an escaped version of URL" do
assert_match /\/spaced%20file\.png/, @dummy.avatar.url
end
end
end
end
require './test/helper'
require 'aws/s3'
unless ENV["S3_TEST_BUCKET"].blank?
class S3LiveTest < Test::Unit::TestCase
context "Using S3 for real, an attachment with S3 storage" do
setup do
rebuild_model :styles => { :thumb => "100x100", :square => "32x32#" },
:storage => :s3,
:bucket => ENV["S3_TEST_BUCKET"],
:path => ":class/:attachment/:id/:style.:extension",
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "..", "s3.yml"))
Dummy.delete_all
@dummy = Dummy.new
end
should "be extended by the S3 module" do
assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3)
end
context "when assigned" do
setup do
@file = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
@dummy.avatar = @file
end
teardown do
@file.close
@dummy.destroy
end
should "still return a Tempfile when sent #to_file" do
assert_equal Paperclip::Tempfile, @dummy.avatar.to_file.class
end
context "and saved" do
setup do
@dummy.save
end
should "be on S3" do
assert true
end
should "generate a tempfile with the right name" do
file = @dummy.avatar.to_file
assert_match /^original.*\.png$/, File.basename(file.path)
end
end
end
end
context "An attachment that uses S3 for storage and has spaces in file name" do
setup do
rebuild_model :styles => { :thumb => "100x100", :square => "32x32#" },
:storage => :s3,
:bucket => ENV["S3_TEST_BUCKET"],
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "..", "s3.yml"))
Dummy.delete_all
@dummy = Dummy.new
@dummy.avatar = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', 'spaced file.png'), 'rb')
@dummy.save
end
teardown { @dummy.destroy }
should "return an unescaped version for path" do
assert_match /.+\/spaced file\.png/, @dummy.avatar.path
end
should "return an escaped version for url" do
assert_match /.+\/spaced%20file\.png/, @dummy.avatar.url
end
should "be accessible" do
assert_match /200 OK/, `curl -I #{@dummy.avatar.url}`
end
should "be destoryable" do
url = @dummy.avatar.url
@dummy.destroy
assert_match /404 Not Found/, `curl -I #{url}`
end
end
end
end
require './test/helper' require './test/helper'
require 'aws/s3' require 'aws/s3'
class StorageTest < Test::Unit::TestCase class S3Test < Test::Unit::TestCase
def rails_env(env) def rails_env(env)
silence_warnings do silence_warnings do
Object.const_set(:Rails, stub('Rails', :env => env)) Object.const_set(:Rails, stub('Rails', :env => env))
end end
end end
context "filesystem" do
setup do
rebuild_model :styles => { :thumbnail => "25x25#" }
@dummy = Dummy.create!
@dummy.avatar = File.open(File.join(File.dirname(__FILE__), "fixtures", "5k.png"))
end
should "allow file assignment" do
assert @dummy.save
end
should "store the original" do
@dummy.save
assert File.exists?(@dummy.avatar.path)
end
should "store the thumbnail" do
@dummy.save
assert File.exists?(@dummy.avatar.path(:thumbnail))
end
end
context "Parsing S3 credentials" do context "Parsing S3 credentials" do
setup do setup do
@proxy_settings = {:host => "127.0.0.1", :port => 8888, :user => "foo", :password => "bar"}
AWS::S3::Base.stubs(:establish_connection!) AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3, rebuild_model :storage => :s3,
:bucket => "testing", :bucket => "testing",
:http_proxy => @proxy_settings,
:s3_credentials => {:not => :important} :s3_credentials => {:not => :important}
@dummy = Dummy.new @dummy = Dummy.new
...@@ -60,6 +39,16 @@ class StorageTest < Test::Unit::TestCase ...@@ -60,6 +39,16 @@ class StorageTest < Test::Unit::TestCase
rails_env("not really an env") rails_env("not really an env")
assert_equal({:test => "12345"}, @avatar.parse_credentials(:test => "12345")) assert_equal({:test => "12345"}, @avatar.parse_credentials(:test => "12345"))
end end
should "support HTTP proxy settings" do
rails_env("development")
assert_equal(true, @avatar.using_http_proxy?)
assert_equal(@proxy_settings[:host], @avatar.http_proxy_host)
assert_equal(@proxy_settings[:port], @avatar.http_proxy_port)
assert_equal(@proxy_settings[:user], @avatar.http_proxy_user)
assert_equal(@proxy_settings[:password], @avatar.http_proxy_password)
end
end end
context "" do context "" do
...@@ -78,6 +67,73 @@ class StorageTest < Test::Unit::TestCase ...@@ -78,6 +67,73 @@ class StorageTest < Test::Unit::TestCase
assert_match %r{^http://s3.amazonaws.com/bucket/avatars/stringio.txt}, @dummy.avatar.url assert_match %r{^http://s3.amazonaws.com/bucket/avatars/stringio.txt}, @dummy.avatar.url
end end
end end
context "s3_host_name" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3,
:s3_credentials => {},
:bucket => "bucket",
:path => ":attachment/:basename.:extension",
:s3_host_name => "s3-ap-northeast-1.amazonaws.com"
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".")
end
should "return a url based on an :s3_host_name path" do
assert_match %r{^http://s3-ap-northeast-1.amazonaws.com/bucket/avatars/stringio.txt}, @dummy.avatar.url
end
end
context "An attachment that uses S3 for storage and has styles that return different file types" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :styles => { :large => ['500x500#', :jpg] },
:storage => :s3,
:bucket => "bucket",
:path => ":attachment/:basename.:extension",
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
@dummy = Dummy.new
@dummy.avatar = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
end
should "return a url containing the correct original file mime type" do
assert_match /.+\/5k.png/, @dummy.avatar.url
end
should "return a url containing the correct processed file mime type" do
assert_match /.+\/5k.jpg/, @dummy.avatar.url(:large)
end
end
context "An attachment that uses S3 for storage and has spaces in file name" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :styles => { :large => ['500x500#', :jpg] },
:storage => :s3,
:bucket => "bucket",
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
@dummy = Dummy.new
@dummy.avatar = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', 'spaced file.png'), 'rb')
end
should "return an unescaped version for path" do
assert_match /.+\/spaced file\.png/, @dummy.avatar.path
end
should "return an escaped version for url" do
assert_match /.+\/spaced%20file\.png/, @dummy.avatar.url
end
end
context "" do context "" do
setup do setup do
AWS::S3::Base.stubs(:establish_connection!) AWS::S3::Base.stubs(:establish_connection!)
...@@ -94,6 +150,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -94,6 +150,7 @@ class StorageTest < Test::Unit::TestCase
assert_match %r{^http://bucket.s3.amazonaws.com/avatars/stringio.txt}, @dummy.avatar.url assert_match %r{^http://bucket.s3.amazonaws.com/avatars/stringio.txt}, @dummy.avatar.url
end end
end end
context "" do context "" do
setup do setup do
AWS::S3::Base.stubs(:establish_connection!) AWS::S3::Base.stubs(:establish_connection!)
...@@ -114,7 +171,54 @@ class StorageTest < Test::Unit::TestCase ...@@ -114,7 +171,54 @@ class StorageTest < Test::Unit::TestCase
end end
end end
context "Generating a url with an expiration" do context "generating a url with a proc as the host alias" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3,
:s3_credentials => { :bucket => "prod_bucket" },
:s3_host_alias => Proc.new{|atch| "cdn#{atch.instance.counter % 4}.example.com"},
:path => ":attachment/:basename.:extension",
:url => ":s3_alias_url"
Dummy.class_eval do
def counter
@counter ||= 0
@counter += 1
@counter
end
end
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".")
end
should "return a url based on the host_alias" do
assert_match %r{^http://cdn1.example.com/avatars/stringio.txt}, @dummy.avatar.url
assert_match %r{^http://cdn2.example.com/avatars/stringio.txt}, @dummy.avatar.url
end
should "still return the bucket name" do
assert_equal "prod_bucket", @dummy.avatar.bucket_name
end
end
context "" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3,
:s3_credentials => {},
:bucket => "bucket",
:path => ":attachment/:basename.:extension",
:url => ":asset_host"
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".")
end
should "return a relative URL for Rails to calculate assets host" do
assert_match %r{^avatars/stringio\.txt}, @dummy.avatar.url
end
end
context "Generating a secure url with an expiration" do
setup do setup do
AWS::S3::Base.stubs(:establish_connection!) AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3, rebuild_model :storage => :s3,
...@@ -123,6 +227,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -123,6 +227,7 @@ class StorageTest < Test::Unit::TestCase
:development => { :bucket => "dev_bucket" } :development => { :bucket => "dev_bucket" }
}, },
:s3_host_alias => "something.something.com", :s3_host_alias => "something.something.com",
:s3_permissions => "private",
:path => ":attachment/:basename.:extension", :path => ":attachment/:basename.:extension",
:url => ":s3_alias_url" :url => ":s3_alias_url"
...@@ -131,9 +236,39 @@ class StorageTest < Test::Unit::TestCase ...@@ -131,9 +236,39 @@ class StorageTest < Test::Unit::TestCase
@dummy = Dummy.new @dummy = Dummy.new
@dummy.avatar = StringIO.new(".") @dummy.avatar = StringIO.new(".")
AWS::S3::S3Object.expects(:url_for).with("avatars/stringio.txt", "prod_bucket", { :expires_in => 3600 }) AWS::S3::S3Object.expects(:url_for).with("avatars/stringio.txt", "prod_bucket", { :expires_in => 3600, :use_ssl => true })
@dummy.avatar.expiring_url
end
should "should succeed" do
assert true
end
end
context "Generating a url with an expiration" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3,
:s3_credentials => {
:production => { :bucket => "prod_bucket" },
:development => { :bucket => "dev_bucket" }
},
:s3_permissions => :private,
:s3_host_alias => "something.something.com",
:path => ":attachment/:style/:basename.:extension",
:url => ":s3_alias_url"
rails_env("production")
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".")
AWS::S3::S3Object.expects(:url_for).with("avatars/original/stringio.txt", "prod_bucket", { :expires_in => 3600, :use_ssl => true })
@dummy.avatar.expiring_url @dummy.avatar.expiring_url
AWS::S3::S3Object.expects(:url_for).with("avatars/thumb/stringio.txt", "prod_bucket", { :expires_in => 1800, :use_ssl => true })
@dummy.avatar.expiring_url(1800, :thumb)
end end
should "should succeed" do should "should succeed" do
...@@ -163,6 +298,33 @@ class StorageTest < Test::Unit::TestCase ...@@ -163,6 +298,33 @@ class StorageTest < Test::Unit::TestCase
end end
end end
context "Parsing S3 credentials with a s3_host_name in them" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
rebuild_model :storage => :s3,
:s3_credentials => {
:production => { :s3_host_name => "s3-world-end.amazonaws.com" },
:development => { :s3_host_name => "s3-ap-northeast-1.amazonaws.com" }
}
@dummy = Dummy.new
end
should "get the right s3_host_name in production" do
rails_env("production")
assert_match %r{^s3-world-end.amazonaws.com}, @dummy.avatar.s3_host_name
end
should "get the right s3_host_name in development" do
rails_env("development")
assert_match %r{^s3-ap-northeast-1.amazonaws.com}, @dummy.avatar.s3_host_name
end
should "get the right s3_host_name if the key does not exist" do
rails_env("test")
assert_match %r{^s3.amazonaws.com}, @dummy.avatar.s3_host_name
end
end
context "An attachment with S3 storage" do context "An attachment with S3 storage" do
setup do setup do
rebuild_model :storage => :s3, rebuild_model :storage => :s3,
...@@ -184,7 +346,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -184,7 +346,7 @@ class StorageTest < Test::Unit::TestCase
context "when assigned" do context "when assigned" do
setup do setup do
@file = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb') @file = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
@dummy = Dummy.new @dummy = Dummy.new
@dummy.avatar = @file @dummy.avatar = @file
end end
...@@ -208,6 +370,15 @@ class StorageTest < Test::Unit::TestCase ...@@ -208,6 +370,15 @@ class StorageTest < Test::Unit::TestCase
end end
end end
should "delete tempfiles" do
AWS::S3::S3Object.stubs(:store).with(@dummy.avatar.path, anything, 'testing', :content_type => 'image/png', :access => :public_read)
File.stubs(:exist?).returns(true)
Paperclip::Tempfile.any_instance.expects(:close).at_least_once()
Paperclip::Tempfile.any_instance.expects(:unlink).at_least_once()
@dummy.save!
end
context "and saved without a bucket" do context "and saved without a bucket" do
setup do setup do
class AWS::S3::NoSuchBucket < AWS::S3::ResponseError class AWS::S3::NoSuchBucket < AWS::S3::ResponseError
...@@ -266,7 +437,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -266,7 +437,7 @@ class StorageTest < Test::Unit::TestCase
context "when assigned" do context "when assigned" do
setup do setup do
@file = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb') @file = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
@dummy = Dummy.new @dummy = Dummy.new
@dummy.avatar = @file @dummy.avatar = @file
end end
...@@ -301,7 +472,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -301,7 +472,7 @@ class StorageTest < Test::Unit::TestCase
rails_env('test') rails_env('test')
rebuild_model :storage => :s3, rebuild_model :storage => :s3,
:s3_credentials => Pathname.new(File.join(File.dirname(__FILE__))).join("fixtures/s3.yml") :s3_credentials => Pathname.new(File.join(File.dirname(__FILE__))).join("../fixtures/s3.yml")
Dummy.delete_all Dummy.delete_all
@dummy = Dummy.new @dummy = Dummy.new
...@@ -323,61 +494,140 @@ class StorageTest < Test::Unit::TestCase ...@@ -323,61 +494,140 @@ class StorageTest < Test::Unit::TestCase
rails_env('test') rails_env('test')
rebuild_model :storage => :s3, rebuild_model :storage => :s3,
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "fixtures/s3.yml")) :s3_credentials => File.new(File.join(File.dirname(__FILE__), "../fixtures/s3.yml"))
Dummy.delete_all Dummy.delete_all
@dummy = Dummy.new @dummy = Dummy.new
end end
should "run it the file through ERB" do should "run the file through ERB" do
assert_equal 'env_bucket', @dummy.avatar.bucket_name assert_equal 'env_bucket', @dummy.avatar.bucket_name
assert_equal 'env_key', AWS::S3::Base.connection.options[:access_key_id] assert_equal 'env_key', AWS::S3::Base.connection.options[:access_key_id]
assert_equal 'env_secret', AWS::S3::Base.connection.options[:secret_access_key] assert_equal 'env_secret', AWS::S3::Base.connection.options[:secret_access_key]
end end
end end
unless ENV["S3_TEST_BUCKET"].blank? context "S3 Permissions" do
context "Using S3 for real, an attachment with S3 storage" do context "defaults to public-read" do
setup do setup do
rebuild_model :styles => { :thumb => "100x100", :square => "32x32#" }, rebuild_model :storage => :s3,
:storage => :s3, :bucket => "testing",
:bucket => ENV["S3_TEST_BUCKET"], :path => ":attachment/:style/:basename.:extension",
:path => ":class/:attachment/:id/:style.:extension", :s3_credentials => {
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "s3.yml")) 'access_key_id' => "12345",
'secret_access_key' => "54321"
}
end
Dummy.delete_all context "when assigned" do
@dummy = Dummy.new setup do
@file = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
teardown { @file.close }
context "and saved" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
AWS::S3::S3Object.expects(:store).with(@dummy.avatar.path,
anything,
'testing',
:content_type => 'image/png',
:access => :public_read)
@dummy.save
end
should "succeed" do
assert true
end
end
end end
end
should "be extended by the S3 module" do context "string permissions set" do
assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3) setup do
rebuild_model :storage => :s3,
:bucket => "testing",
:path => ":attachment/:style/:basename.:extension",
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
:s3_permissions => 'private'
end end
context "when assigned" do context "when assigned" do
setup do setup do
@file = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb') @file = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file @dummy.avatar = @file
end end
teardown { @file.close } teardown { @file.close }
should "still return a Tempfile when sent #to_file" do
assert_equal Paperclip::Tempfile, @dummy.avatar.to_file.class
end
context "and saved" do context "and saved" do
setup do setup do
AWS::S3::Base.stubs(:establish_connection!)
AWS::S3::S3Object.expects(:store).with(@dummy.avatar.path,
anything,
'testing',
:content_type => 'image/png',
:access => 'private')
@dummy.save @dummy.save
end end
should "be on S3" do should "succeed" do
assert true assert true
end end
end
end
end
context "hash permissions set" do
setup do
rebuild_model :storage => :s3,
:bucket => "testing",
:path => ":attachment/:style/:basename.:extension",
:styles => {
:thumb => "80x80>"
},
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
:s3_permissions => {
:original => 'private',
:thumb => 'public-read'
}
end
context "when assigned" do
setup do
@file = File.new(File.join(File.dirname(__FILE__), '..', 'fixtures', '5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
teardown { @file.close }
context "and saved" do
setup do
AWS::S3::Base.stubs(:establish_connection!)
[:thumb, :original].each do |style|
AWS::S3::S3Object.expects(:store).with("avatars/#{style}/5k.png",
anything,
'testing',
:content_type => 'image/png',
:access => style == :thumb ? 'public-read' : 'private')
end
@dummy.save
end
should "generate a tempfile with the right name" do should "succeed" do
file = @dummy.avatar.to_file assert true
assert_match /^original.*\.png$/, File.basename(file.path)
end end
end end
end end
......
...@@ -6,8 +6,9 @@ class StyleTest < Test::Unit::TestCase ...@@ -6,8 +6,9 @@ class StyleTest < Test::Unit::TestCase
context "A style rule" do context "A style rule" do
setup do setup do
@attachment = attachment :path => ":basename.:extension", @attachment = attachment :path => ":basename.:extension",
:styles => { :foo => {:geometry => "100x100#", :format => :png} } :styles => { :foo => {:geometry => "100x100#", :format => :png} },
@style = @attachment.styles[:foo] :whiny => true
@style = @attachment.options.styles[:foo]
end end
should "be held as a Style object" do should "be held as a Style object" do
...@@ -23,7 +24,6 @@ class StyleTest < Test::Unit::TestCase ...@@ -23,7 +24,6 @@ class StyleTest < Test::Unit::TestCase
end end
should "be whiny if the attachment is" do should "be whiny if the attachment is" do
@attachment.expects(:whiny).returns(true)
assert @style.whiny? assert @style.whiny?
end end
...@@ -46,52 +46,49 @@ class StyleTest < Test::Unit::TestCase ...@@ -46,52 +46,49 @@ class StyleTest < Test::Unit::TestCase
} }
end end
should "defer processing of procs until they are needed" do
assert_kind_of Proc, @attachment.styles[:foo].instance_variable_get("@geometry")
assert_kind_of Proc, @attachment.styles[:bar].instance_variable_get("@geometry")
assert_kind_of Proc, @attachment.instance_variable_get("@processors")
end
should "call procs when they are needed" do should "call procs when they are needed" do
assert_equal "300x300#", @attachment.styles[:foo].geometry assert_equal "300x300#", @attachment.options.styles[:foo].geometry
assert_equal "300x300#", @attachment.styles[:bar].geometry assert_equal "300x300#", @attachment.options.styles[:bar].geometry
assert_equal [:test], @attachment.styles[:foo].processors assert_equal [:test], @attachment.options.styles[:foo].processors
assert_equal [:test], @attachment.styles[:bar].processors assert_equal [:test], @attachment.options.styles[:bar].processors
end end
end end
context "An attachment with style rules in various forms" do context "An attachment with style rules in various forms" do
setup do setup do
styles = ActiveSupport::OrderedHash.new
styles[:aslist] = ["100x100", :png]
styles[:ashash] = {:geometry => "100x100", :format => :png}
styles[:asstring] = "100x100"
@attachment = attachment :path => ":basename.:extension", @attachment = attachment :path => ":basename.:extension",
:styles => { :styles => styles
:aslist => ["100x100", :png],
:ashash => {:geometry => "100x100", :format => :png},
:asstring => "100x100"
}
end end
should "have the right number of styles" do should "have the right number of styles" do
assert_kind_of Hash, @attachment.styles assert_kind_of Hash, @attachment.options.styles
assert_equal 3, @attachment.styles.size assert_equal 3, @attachment.options.styles.size
end end
should "have styles as Style objects" do should "have styles as Style objects" do
[:aslist, :ashash, :aslist].each do |s| [:aslist, :ashash, :aslist].each do |s|
assert_kind_of Paperclip::Style, @attachment.styles[s] assert_kind_of Paperclip::Style, @attachment.options.styles[s]
end end
end end
should "have the right geometries" do should "have the right geometries" do
[:aslist, :ashash, :aslist].each do |s| [:aslist, :ashash, :aslist].each do |s|
assert_equal @attachment.styles[s].geometry, "100x100" assert_equal @attachment.options.styles[s].geometry, "100x100"
end end
end end
should "have the right formats" do should "have the right formats" do
assert_equal @attachment.styles[:aslist].format, :png assert_equal @attachment.options.styles[:aslist].format, :png
assert_equal @attachment.styles[:ashash].format, :png assert_equal @attachment.options.styles[:ashash].format, :png
assert_nil @attachment.styles[:asstring].format assert_nil @attachment.options.styles[:asstring].format
end end
should "retain order" do
assert_equal [:aslist, :ashash, :asstring], @attachment.options.styles.keys
end
end end
context "An attachment with :convert_options" do context "An attachment with :convert_options" do
...@@ -99,7 +96,7 @@ class StyleTest < Test::Unit::TestCase ...@@ -99,7 +96,7 @@ class StyleTest < Test::Unit::TestCase
@attachment = attachment :path => ":basename.:extension", @attachment = attachment :path => ":basename.:extension",
:styles => {:thumb => "100x100", :large => "400x400"}, :styles => {:thumb => "100x100", :large => "400x400"},
:convert_options => {:all => "-do_stuff", :thumb => "-thumbnailize"} :convert_options => {:all => "-do_stuff", :thumb => "-thumbnailize"}
@style = @attachment.styles[:thumb] @style = @attachment.options.styles[:thumb]
@file = StringIO.new("...") @file = StringIO.new("...")
@file.stubs(:original_filename).returns("file.jpg") @file.stubs(:original_filename).returns("file.jpg")
end end
...@@ -110,7 +107,27 @@ class StyleTest < Test::Unit::TestCase ...@@ -110,7 +107,27 @@ class StyleTest < Test::Unit::TestCase
should "call extra_options_for(:thumb/:large) when convert options are requested" do should "call extra_options_for(:thumb/:large) when convert options are requested" do
@attachment.expects(:extra_options_for).with(:thumb) @attachment.expects(:extra_options_for).with(:thumb)
@attachment.styles[:thumb].convert_options @attachment.options.styles[:thumb].convert_options
end
end
context "An attachment with :source_file_options" do
setup do
@attachment = attachment :path => ":basename.:extension",
:styles => {:thumb => "100x100", :large => "400x400"},
:source_file_options => {:all => "-density 400", :thumb => "-depth 8"}
@style = @attachment.options.styles[:thumb]
@file = StringIO.new("...")
@file.stubs(:original_filename).returns("file.jpg")
end
before_should "not have called extra_source_file_options_for(:thumb/:large) on initialization" do
@attachment.expects(:extra_source_file_options_for).never
end
should "call extra_options_for(:thumb/:large) when convert options are requested" do
@attachment.expects(:extra_source_file_options_for).with(:thumb)
@attachment.options.styles[:thumb].source_file_options
end end
end end
...@@ -125,7 +142,7 @@ class StyleTest < Test::Unit::TestCase ...@@ -125,7 +142,7 @@ class StyleTest < Test::Unit::TestCase
} }
}, },
:processors => [:thumbnail] :processors => [:thumbnail]
@style = @attachment.styles[:foo] @style = @attachment.options.styles[:foo]
end end
should "not get processors from the attachment" do should "not get processors from the attachment" do
...@@ -138,4 +155,26 @@ class StyleTest < Test::Unit::TestCase ...@@ -138,4 +155,26 @@ class StyleTest < Test::Unit::TestCase
end end
end end
context "A style rule with :processors supplied as procs" do
setup do
@attachment = attachment :path => ":basename.:extension",
:styles => {
:foo => {
:geometry => "100x100#",
:format => :png,
:processors => lambda{|a| [:test]}
}
},
:processors => [:thumbnail]
end
should "defer processing of procs until they are needed" do
assert_kind_of Proc, @attachment.options.styles[:foo].instance_variable_get("@processors")
end
should "call procs when they are needed" do
assert_equal [:test], @attachment.options.styles[:foo].processors
end
end
end end
...@@ -73,6 +73,18 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -73,6 +73,18 @@ class ThumbnailTest < Test::Unit::TestCase
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "100x50#") @thumb = Paperclip::Thumbnail.new(@file, :geometry => "100x50#")
end end
should "let us know when a command isn't found versus a processing error" do
old_path = ENV['PATH']
begin
ENV['PATH'] = ''
assert_raises(Paperclip::CommandNotFoundError) do
@thumb.make
end
ensure
ENV['PATH'] = old_path
end
end
should "report its correct current and target geometries" do should "report its correct current and target geometries" do
assert_equal "100x50#", @thumb.target_geometry.to_s assert_equal "100x50#", @thumb.target_geometry.to_s
assert_equal "434x66", @thumb.current_geometry.to_s assert_equal "434x66", @thumb.current_geometry.to_s
...@@ -90,9 +102,15 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -90,9 +102,15 @@ class ThumbnailTest < Test::Unit::TestCase
assert_equal nil, @thumb.convert_options assert_equal nil, @thumb.convert_options
end end
should "have source_file_options set to nil by default" do
assert_equal nil, @thumb.source_file_options
end
should "send the right command to convert when sent #make" do should "send the right command to convert when sent #make" do
Paperclip::CommandLine.expects(:"`").with do |arg| Paperclip.expects(:run).with do |*arg|
arg.match %r{convert ["']#{File.expand_path(@thumb.file.path)}\[0\]["'] -resize ["']x50["'] -crop ["']100x50\+114\+0["'] \+repage ["'].*?["']} arg[0] == 'convert' &&
arg[1] == ':source -resize "x50" -crop "100x50+114+0" +repage :dest' &&
arg[2][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
end end
@thumb.make @thumb.make
end end
...@@ -115,8 +133,10 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -115,8 +133,10 @@ class ThumbnailTest < Test::Unit::TestCase
end end
should "send the right command to convert when sent #make" do should "send the right command to convert when sent #make" do
Paperclip::CommandLine.expects(:"`").with do |arg| Paperclip.expects(:run).with do |*arg|
arg.match %r{convert -strip ["']#{File.expand_path(@thumb.file.path)}\[0\]["'] -resize ["']x50["'] -crop ["']100x50\+114\+0["'] \+repage ["'].*?["']} arg[0] == 'convert' &&
arg[1] == '-strip :source -resize "x50" -crop "100x50+114+0" +repage :dest' &&
arg[2][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
end end
@thumb.make @thumb.make
end end
...@@ -153,8 +173,10 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -153,8 +173,10 @@ class ThumbnailTest < Test::Unit::TestCase
end end
should "send the right command to convert when sent #make" do should "send the right command to convert when sent #make" do
Paperclip::CommandLine.expects(:"`").with do |arg| Paperclip.expects(:run).with do |*arg|
arg.match %r{convert ["']#{File.expand_path(@thumb.file.path)}\[0\]["'] -resize ["']x50["'] -crop ["']100x50\+114\+0["'] \+repage -strip -depth 8 ["'].*?["']} arg[0] == 'convert' &&
arg[1] == ':source -resize "x50" -crop "100x50+114+0" +repage -strip -depth 8 :dest' &&
arg[2][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
end end
@thumb.make @thumb.make
end end
...@@ -176,6 +198,18 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -176,6 +198,18 @@ class ThumbnailTest < Test::Unit::TestCase
@thumb.make @thumb.make
end end
end end
should "let us know when a command isn't found versus a processing error" do
old_path = ENV['PATH']
begin
ENV['PATH'] = ''
assert_raises(Paperclip::CommandNotFoundError) do
@thumb.make
end
ensure
ENV['PATH'] = old_path
end
end
end end
end end
...@@ -190,6 +224,53 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -190,6 +224,53 @@ class ThumbnailTest < Test::Unit::TestCase
assert !@thumb.transformation_command.include?("-resize") assert !@thumb.transformation_command.include?("-resize")
end end
end end
context "passing a custom file geometry parser" do
should "produce the appropriate transformation_command" do
GeoParser = Class.new do
def self.from_file(file)
new
end
def transformation_to(target, should_crop)
["SCALE", "CROP"]
end
end
thumb = Paperclip::Thumbnail.new(@file, :geometry => '50x50', :file_geometry_parser => GeoParser)
transformation_command = thumb.transformation_command
assert transformation_command.include?('-crop'),
%{expected #{transformation_command.inspect} to include '-crop'}
assert transformation_command.include?('"CROP"'),
%{expected #{transformation_command.inspect} to include '"CROP"'}
assert transformation_command.include?('-resize'),
%{expected #{transformation_command.inspect} to include '-resize'}
assert transformation_command.include?('"SCALE"'),
%{expected #{transformation_command.inspect} to include '"SCALE"'}
end
end
context "passing a custom geometry string parser" do
should "produce the appropriate transformation_command" do
GeoParser = Class.new do
def self.parse(s)
new
end
def to_s
"151x167"
end
end
thumb = Paperclip::Thumbnail.new(@file, :geometry => '50x50', :string_geometry_parser => GeoParser)
transformation_command = thumb.transformation_command
assert transformation_command.include?('"151x167"'),
%{expected #{transformation_command.inspect} to include '151x167'}
end
end
end end
context "A multipage PDF" do context "A multipage PDF" do
...@@ -224,4 +305,79 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -224,4 +305,79 @@ class ThumbnailTest < Test::Unit::TestCase
end end
end end
end end
context "An animated gif" do
setup do
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "animated.gif"), 'rb')
end
teardown { @file.close }
should "start with 12 frames with size 100x100" do
cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
assert_equal "100x100"*12, `#{cmd}`.chomp
end
context "with static output" do
setup do
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50", :format => :jpg)
end
should "create the single frame thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
assert_equal "50x50", `#{cmd}`.chomp
end
end
context "with animated output format" do
setup do
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50", :format => :gif)
end
should "create the 12 frames thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
assert_equal "50x50"*12, `#{cmd}`.chomp
end
should "use the -coalesce option" do
assert_equal @thumb.transformation_command.first, "-coalesce"
end
end
context "with omitted output format" do
setup do
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50")
end
should "create the 12 frames thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
assert_equal "50x50"*12, `#{cmd}`.chomp
end
should "use the -coalesce option" do
assert_equal @thumb.transformation_command.first, "-coalesce"
end
end
context "with animated option set to false" do
setup do
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "50x50", :animated => false)
end
should "output the gif format" do
dst = @thumb.make
cmd = %Q[identify "#{dst.path}"]
assert_match /GIF/, `#{cmd}`.chomp
end
should "create the single frame thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
assert_equal "50x50", `#{cmd}`.chomp
end
end
end
end end
...@@ -6,12 +6,13 @@ class UpfileTest < Test::Unit::TestCase ...@@ -6,12 +6,13 @@ class UpfileTest < Test::Unit::TestCase
%w(png) => 'image/png', %w(png) => 'image/png',
%w(gif) => 'image/gif', %w(gif) => 'image/gif',
%w(bmp) => 'image/bmp', %w(bmp) => 'image/bmp',
%w(svg) => 'image/svg+xml',
%w(txt) => 'text/plain', %w(txt) => 'text/plain',
%w(htm html) => 'text/html', %w(htm html) => 'text/html',
%w(csv) => 'text/csv', %w(csv) => 'text/csv',
%w(xml) => 'text/xml', %w(xml) => 'application/xml',
%w(css) => 'text/css', %w(css) => 'text/css',
%w(js) => 'application/js', %w(js) => 'application/javascript',
%w(foo) => 'application/x-foo' %w(foo) => 'application/x-foo'
}.each do |extensions, content_type| }.each do |extensions, content_type|
extensions.each do |extension| extensions.each do |extension|
...@@ -33,4 +34,20 @@ class UpfileTest < Test::Unit::TestCase ...@@ -33,4 +34,20 @@ class UpfileTest < Test::Unit::TestCase
end end
assert_equal 'text/plain', file.content_type assert_equal 'text/plain', file.content_type
end end
{ '5k.png' => 'image/png',
'animated.gif' => 'image/gif',
'text.txt' => 'text/plain',
'twopage.pdf' => 'application/pdf'
}.each do |filename, content_type|
should "return a content type of #{content_type} from a file command for file #{filename}" do
file = File.new(File.join(File.dirname(__FILE__), "fixtures", filename))
class << file
include Paperclip::Upfile
end
assert_equal content_type, file.type_from_file_command
end
end
end end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment