Commit 237597eb by Mike Mondragon

Merge remote branch 'upstream/master'

Conflicts:
	lib/paperclip/upfile.rb
	paperclip.gemspec
parents 3eb7b902 cbfaca48
...@@ -3,3 +3,9 @@ ...@@ -3,3 +3,9 @@
tmp tmp
test/s3.yml test/s3.yml
public public
paperclip*.gem
capybara*.html
*.rbc
.bundle
*SPIKE*
.rvmrc
rvm:
- 1.8.7
- 1.9.2
- ree
- rbx-2.0
script: "bundle exec rake clean test"
gemfile:
- gemfiles/rails2.gemfile
- gemfiles/rails3.gemfile
- gemfiles/rails3_1.gemfile
appraise "rails2" do
gem "rails", "~> 2.3.12"
end
appraise "rails3" do
gem "rails", "~> 3.0.9"
end
appraise "rails3_1" do
gem "rails", "~> 3.1.0.rc5"
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"
gem "activerecord", :require => "active_record"
gem "appraisal"
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 "shoulda"
gem "sqlite3", "~>1.3.4"
# This is for Rails 3.1
gem "sprockets", "~> 2.0.0.beta.13", :require => false
# gem "ruby-debug", :platform => :ruby_18
# gem "ruby-debug19", :platform => :ruby_19
GEM
remote: http://rubygems.org/
specs:
activerecord (2.3.12)
activesupport (= 2.3.12)
activesupport (2.3.12)
appraisal (0.3.5)
aruba (~> 0.3.6)
bundler
rake
aruba (0.3.7)
childprocess (>= 0.1.9)
cucumber (>= 0.10.5)
rspec (>= 2.6.0)
aws-s3 (0.6.2)
builder
mime-types
xml-simple
bouncy-castle-java (1.5.0146.1)
builder (3.0.0)
childprocess (0.1.9)
ffi (~> 1.0.6)
cocaine (0.2.0)
cucumber (0.10.5)
builder (>= 2.1.2)
diff-lcs (>= 1.1.2)
gherkin (~> 2.4.0)
json (>= 1.4.6)
term-ansicolor (>= 1.0.5)
diff-lcs (1.1.2)
excon (0.6.3)
ffi (1.0.9)
ffi (1.0.9-java)
fog (0.8.2)
builder
excon (~> 0.6.1)
formatador (>= 0.1.3)
json
mime-types
net-ssh (>= 2.1.3)
nokogiri (>= 1.4.4)
ruby-hmac
formatador (0.1.4)
gherkin (2.4.5)
json (>= 1.4.6)
gherkin (2.4.5-java)
json (>= 1.4.6)
hike (1.2.0)
jruby-openssl (0.7.4)
bouncy-castle-java
json (1.5.3)
json (1.5.3-java)
mime-types (1.16)
mocha (0.9.12)
net-ssh (2.1.4)
nokogiri (1.4.4)
nokogiri (1.4.4-java)
weakling (>= 0.0.3)
rack (1.3.2)
rake (0.9.2)
rdoc (3.8)
rspec (2.6.0)
rspec-core (~> 2.6.0)
rspec-expectations (~> 2.6.0)
rspec-mocks (~> 2.6.0)
rspec-core (2.6.4)
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
ruby-hmac (0.4.0)
shoulda (2.11.3)
sprockets (2.0.0.beta.13)
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.4)
term-ansicolor (1.0.5)
tilt (1.3.2)
weakling (0.0.4-java)
xml-simple (1.0.16)
PLATFORMS
java
ruby
DEPENDENCIES
activerecord
appraisal
aws-s3
bundler
cocaine (~> 0.2)
fog
jruby-openssl
mime-types
mocha
rake
rdoc
shoulda
sprockets (~> 2.0.0.beta.13)
sqlite3 (~> 1.3.4)
=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
...@@ -12,10 +12,57 @@ packages). Attached files are saved to the filesystem and referenced in the ...@@ -12,10 +12,57 @@ packages). Attached files are saved to the filesystem and referenced in the
browser by an easily understandable specification, which has sensible and browser by an easily understandable specification, which has sensible and
useful defaults. useful defaults.
See the documentation for +has_attached_file+ in Paperclip::ClassMethods for See the documentation for `has_attached_file` in Paperclip::ClassMethods for
more detailed options. more detailed options.
==Quick Start The complete [RDoc](http://rdoc.info/gems/paperclip) is online.
Requirements
------------
ImageMagick must be installed and Paperclip must have access to it. To ensure
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
example, it might return `/usr/local/bin/convert`.
Then, in your environment config file, let Paperclip know to look there by adding that
directory to its path.
In development mode, you might add this line to `config/environments/development.rb)`:
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
------------
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:
gem "paperclip", "~> 2.3"
Or, if you don't use Bundler (though you probably should, even in Rails 2), with config.gem
# In config/environment.rb
...
Rails::Initializer.run do |config|
...
config.gem "paperclip", :version => "~> 2.3"
...
end
Quick Start
-----------
In your model: In your model:
...@@ -59,15 +106,21 @@ In your show view: ...@@ -59,15 +106,21 @@ 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) %>
==Usage To detach a file, simply set the attribute to `nil`:
@user.avatar = nil
@user.save
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
information about the options to has_attached_file is available in the information about the options to has_attached_file is available in the
documentation of Paperclip::ClassMethods. documentation of Paperclip::ClassMethods.
...@@ -75,7 +128,8 @@ Attachments can be validated with Paperclip's validation methods, ...@@ -75,7 +128,8 @@ Attachments can be validated with Paperclip's validation methods,
validates_attachment_presence, validates_attachment_content_type, and validates_attachment_presence, validates_attachment_content_type, and
validates_attachment_size. validates_attachment_size.
==Storage Storage
-------
The files that are assigned as attachments are, by default, placed in the The files that are assigned as attachments are, by default, placed in the
directory specified by the :path option to has_attached_file. By default, this directory specified by the :path option to has_attached_file. By default, this
...@@ -87,8 +141,8 @@ file at ...@@ -87,8 +141,8 @@ file at
/data/myapp/releases/20081229172410/public/system/avatars/13/small/my_pic.png /data/myapp/releases/20081229172410/public/system/avatars/13/small/my_pic.png
NOTE: This is a change from previous versions of Paperclip, but is overall a _NOTE: This is a change from previous versions of Paperclip, but is overall a
safer choice for the default file store. safer choice for the default file store._
You may also choose to store your files using Amazon's S3 service. You can find You may also choose to store your files using Amazon's S3 service. You can find
more information about S3 storage at the description for more information about S3 storage at the description for
...@@ -101,7 +155,8 @@ both the :path and :url options in order to make sure the files are unavailable ...@@ -101,7 +155,8 @@ both the :path and :url options in order to make sure the files are unavailable
to the public. Both :path and :url allow the same set of interpolated to the public. Both :path and :url allow the same set of interpolated
variables. variables.
==Post Processing Post Processing
---------------
Paperclip supports an extensible selection of post-processors. When you define Paperclip supports an extensible selection of post-processors. When you define
a set of styles for an attachment, by default it is expected that those a set of styles for an attachment, by default it is expected that those
...@@ -142,33 +197,74 @@ then both the :rotator processor and the :ocr processor would receive the ...@@ -142,33 +197,74 @@ then both the :rotator processor and the :ocr processor would receive the
options "{ :quality => :better }". This parameter may not mean anything to one options "{ :quality => :better }". This parameter may not mean anything to one
or more or the processors, and they are expected to ignore it. 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.
==Events Also, if you're interesting to generate 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
------
Before and after the Post Processing step, Paperclip calls back to the model Before and after the Post Processing step, Paperclip calls back to the model
with a few callbacks, allowing the model to change or cancel the processing with a few callbacks, allowing the model to change or cancel the processing
step. The callbacks are "before_post_process" and "after_post_process" (which step. The callbacks are `before_post_process` and `after_post_process` (which
are called before and after the processing of each attachment), and the are called before and after the processing of each attachment), and the
attachment-specific "before_<attachment>_post_process" and attachment-specific `before_<attachment>_post_process` and
"after_<attachment>_post_process". The callbacks are intended to be as close to `after_<attachment>_post_process`. The callbacks are intended to be as close to
normal ActiveRecord callbacks as possible, so if you return false (specifically normal ActiveRecord callbacks as possible, so if you return false (specifically
- returning nil is not the same) in a before_ filter, the post processing step - returning nil is not the same) in a before_ filter, the post processing step
will halt. Returning false in an after_ filter will not halt anything, but you will halt. Returning false in an after_ filter will not halt anything, but you
can access the model and the attachment if necessary. can access the model and the attachment if necessary.
NOTE: Post processing will not even *start* if the attachment is not valid _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. For more on this feature read author's own explanation.
[https://github.com/thoughtbot/paperclip/pull/416](https://github.com/thoughtbot/paperclip/pull/416)
Testing
-------
Paperclip provides rspec-compatible matchers for testing attachments. See the
documentation on [Paperclip::Shoulda::Matchers](http://rubydoc.info/gems/paperclip/Paperclip/Shoulda/Matchers)
for more information.
==Contributing Contributing
------------
If you'd like to contribute a feature or bugfix: Thanks! To make sure your 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, or post a new GitHub Issue. 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].
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
-------
![thoughtbot](http://thoughtbot.com/images/tm/logo.png)
Paperclip is maintained and funded by [thoughtbot, inc](http://thoughtbot.com/community)
Thank you to all [the contributors](https://github.com/thoughtbot/paperclip/contributors)!
The names and logos for thoughtbot are trademarks of thoughtbot, inc.
License
-------
Paperclip is Copyright © 2008-2011 thoughtbot. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
require 'rubygems'
require 'appraisal'
require 'bundler/setup'
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
require 'rake/rdoctask' require 'rdoc/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, :test] task :default => [:clean, 'appraisal:install', :all]
desc 'Test the paperclip plugin under all supported Rails versions.'
task :all do |t|
exec('rake appraisal test')
end
desc 'Test the paperclip plugin.' desc 'Test the paperclip plugin.'
Rake::TestTask.new(:test) do |t| Rake::TestTask.new(:test) do |t|
...@@ -22,7 +31,7 @@ task :shell do |t| ...@@ -22,7 +31,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'
...@@ -40,47 +49,15 @@ task :clean do |t| ...@@ -40,47 +49,15 @@ task :clean do |t|
FileUtils.rm_rf "doc" FileUtils.rm_rf "doc"
FileUtils.rm_rf "tmp" FileUtils.rm_rf "tmp"
FileUtils.rm_rf "pkg" FileUtils.rm_rf "pkg"
FileUtils.rm_rf "public"
FileUtils.rm "test/debug.log" rescue nil FileUtils.rm "test/debug.log" rescue nil
FileUtils.rm "test/paperclip.db" rescue nil FileUtils.rm "test/paperclip.db" rescue nil
Dir.glob("paperclip-*.gem").each{|f| FileUtils.rm f } Dir.glob("paperclip-*.gem").each{|f| FileUtils.rm f }
end end
include_file_globs = ["README*", desc 'Build the gemspec.'
"LICENSE", task :gemspec do |t|
"Rakefile", exec 'gem build paperclip.gemspec'
"init.rb",
"{generators,lib,tasks,test,shoulda_macros}/**/*"]
exclude_file_globs = ["test/s3.yml",
"test/debug.log",
"test/paperclip.db",
"test/doc",
"test/doc/*",
"test/pkg",
"test/pkg/*",
"test/tmp",
"test/tmp/*"]
spec = Gem::Specification.new do |s|
s.name = "paperclip"
s.version = Paperclip::VERSION
s.author = "Jon Yurek"
s.email = "jyurek@thoughtbot.com"
s.homepage = "http://www.thoughtbot.com/projects/paperclip"
s.platform = Gem::Platform::RUBY
s.summary = "File attachments as attributes for ActiveRecord"
s.files = FileList[include_file_globs].to_a - FileList[exclude_file_globs].to_a
s.require_path = "lib"
s.test_files = FileList["test/**/test_*.rb"].to_a
s.rubyforge_project = "paperclip"
s.has_rdoc = true
s.extra_rdoc_files = FileList["README*"].to_a
s.rdoc_options << '--line-numbers' << '--inline-source'
s.requirements << "ImageMagick"
s.add_development_dependency 'shoulda'
s.add_development_dependency 'jferris-mocha', '>= 0.9.5.0.1241126838'
s.add_development_dependency 'aws-s3'
s.add_development_dependency 'sqlite3-ruby'
s.add_development_dependency 'activerecord'
s.add_development_dependency 'activesupport'
end end
desc "Print a list of the files to be put into the gem" desc "Print a list of the files to be put into the gem"
......
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: 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"
Then %r{I should see an image with a path of "([^"]*)"} do |path|
page.should have_css("img[src^='#{path}']")
end
Then %r{^the file at "([^"]*)" is the same as "([^"]*)"$} do |web_file, path|
expected = IO.read(path)
actual = if web_file.match %r{^https?://}
Net::HTTP.get(URI.parse(web_file))
else
visit(web_file)
page.body
end
actual.should == expected
end
Given "I have a rails application" do
steps %{
Given I generate a rails application
And this plugin is available
And I have a "users" resource with "name:string"
And I turn off class caching
Given I save the following as "app/models/user.rb"
"""
class User < ActiveRecord::Base
end
"""
And I save the following as "config/s3.yml"
"""
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
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"
"""
<p>Name: <%= @user.name %></p>
<p>Avatar: <%= image_tag @user.avatar.url %></p>
"""
And I run "script/generate paperclip user avatar"
And the rails application is prepped and running
}
end
Given %r{I generate a rails application} do
FileUtils.rm_rf TEMP_ROOT
FileUtils.mkdir_p TEMP_ROOT
Dir.chdir(TEMP_ROOT) do
`rails _2.3.8_ #{APP_NAME}`
end
end
When %r{I save the following as "([^"]*)"} do |path, string|
FileUtils.mkdir_p(File.join(CUC_RAILS_ROOT, File.dirname(path)))
File.open(File.join(CUC_RAILS_ROOT, path), 'w') { |file| file.write(string) }
end
When %r{I turn off class caching} do
Dir.chdir(CUC_RAILS_ROOT) do
file = "config/environments/test.rb"
config = IO.read(file)
config.gsub!(%r{^\s*config.cache_classes.*$},
"config.cache_classes = false")
File.open(file, "w"){|f| f.write(config) }
end
end
When %r{the rails application is prepped and running$} do
When "I reset the database"
When "the rails application is running"
end
When %r{I reset the database} do
When %{I run "rake db:drop db:create db:migrate"}
end
When %r{the rails application is running} do
Dir.chdir(CUC_RAILS_ROOT) do
require "config/environment"
require "capybara/rails"
end
end
When %r{this plugin is available} do
$LOAD_PATH << "#{PROJECT_ROOT}/lib"
require 'paperclip'
When %{I save the following as "vendor/plugins/paperclip/rails/init.rb"},
IO.read("#{PROJECT_ROOT}/rails/init.rb")
end
When %r{I run "([^"]*)"} do |command|
Dir.chdir(CUC_RAILS_ROOT) do
`#{command}`
end
end
When %r{I have a "([^"]*)" resource with "([^"]*)"} do |resource, fields|
When %{I run "script/generate scaffold #{resource} #{fields}"}
end
Given /I validate my S3 credentials/ do
key = ENV['AWS_ACCESS_KEY_ID']
secret = ENV['AWS_SECRET_ACCESS_KEY']
key.should_not be_nil
secret.should_not be_nil
assert_credentials(key, secret)
end
# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
# 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
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
# files.
require 'uri'
require 'cgi'
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
module WithinHelpers
def with_scope(locator)
locator ? within(locator) { yield } : yield
end
end
World(WithinHelpers)
Given /^(?:|I )am on (.+)$/ do |page_name|
visit path_to(page_name)
end
When /^(?:|I )go to (.+)$/ do |page_name|
visit path_to(page_name)
end
When /^(?:|I )visit (\/.+)$/ do |page_path|
visit page_path
end
When /^(?:|I )press "([^"]*)"(?: within "([^"]*)")?$/ do |button, selector|
with_scope(selector) do
click_button(button)
end
end
When /^(?:|I )follow "([^"]*)"(?: within "([^"]*)")?$/ do |link, selector|
with_scope(selector) do
click_link(link)
end
end
When /^(?:|I )fill in "([^"]*)" with "([^"]*)"(?: within "([^"]*)")?$/ do |field, value, selector|
with_scope(selector) do
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
# Use this to fill in an entire form with data from a table. Example:
#
# When I fill in the following:
# | Account Number | 5002 |
# | Expiry date | 2009-11-01 |
# | Note | Nice guy |
# | Wants Email? | |
#
# TODO: Add support for checkbox, select og option
# based on naming conventions.
#
When /^(?:|I )fill in the following(?: within "([^"]*)")?:$/ do |selector, fields|
with_scope(selector) do
fields.rows_hash.each do |name, value|
When %{I fill in "#{name}" with "#{value}"}
end
end
end
When /^(?:|I )select "([^"]*)" from "([^"]*)"(?: within "([^"]*)")?$/ do |value, field, selector|
with_scope(selector) do
select(value, :from => field)
end
end
When /^(?:|I )check "([^"]*)"(?: within "([^"]*)")?$/ do |field, selector|
with_scope(selector) do
check(field)
end
end
When /^(?:|I )uncheck "([^"]*)"(?: within "([^"]*)")?$/ do |field, selector|
with_scope(selector) do
uncheck(field)
end
end
When /^(?:|I )choose "([^"]*)"(?: within "([^"]*)")?$/ do |field, selector|
with_scope(selector) do
choose(field)
end
end
When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"(?: within "([^"]*)")?$/ do |path, field, selector|
with_scope(selector) do
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
Then /^(?:|I )should see "([^"]*)"(?: within "([^"]*)")?$/ do |text, selector|
with_scope(selector) do
if page.respond_to? :should
page.should have_content(text)
else
assert page.has_content?(text)
end
end
end
Then /^(?:|I )should see \/([^\/]*)\/(?: within "([^"]*)")?$/ do |regexp, selector|
regexp = Regexp.new(regexp)
with_scope(selector) do
if page.respond_to? :should
page.should have_xpath('//*', :text => regexp)
else
assert page.has_xpath?('//*', :text => regexp)
end
end
end
Then /^(?:|I )should not see "([^"]*)"(?: within "([^"]*)")?$/ do |text, selector|
with_scope(selector) do
if page.respond_to? :should
page.should have_no_content(text)
else
assert page.has_no_content?(text)
end
end
end
Then /^(?:|I )should not see \/([^\/]*)\/(?: within "([^"]*)")?$/ do |regexp, selector|
regexp = Regexp.new(regexp)
with_scope(selector) do
if page.respond_to? :should
page.should have_no_xpath('//*', :text => regexp)
else
assert page.has_no_xpath?('//*', :text => regexp)
end
end
end
Then /^the "([^"]*)" field(?: within "([^"]*)")? should contain "([^"]*)"$/ do |field, selector, value|
with_scope(selector) do
field = find_field(field)
field_value = (field.tag_name == 'textarea') ? field.text : field.value
if field_value.respond_to? :should
field_value.should =~ /#{value}/
else
assert_match(/#{value}/, field_value)
end
end
end
Then /^the "([^"]*)" field(?: within "([^"]*)")? should not contain "([^"]*)"$/ do |field, selector, value|
with_scope(selector) do
field = find_field(field)
field_value = (field.tag_name == 'textarea') ? field.text : field.value
if field_value.respond_to? :should_not
field_value.should_not =~ /#{value}/
else
assert_no_match(/#{value}/, field_value)
end
end
end
Then /^the "([^"]*)" checkbox(?: within "([^"]*)")? should be checked$/ do |label, selector|
with_scope(selector) do
field_checked = find_field(label)['checked']
if field_checked.respond_to? :should
field_checked.should be_true
else
assert field_checked
end
end
end
Then /^the "([^"]*)" checkbox(?: within "([^"]*)")? should not be checked$/ do |label, selector|
with_scope(selector) do
field_checked = find_field(label)['checked']
if field_checked.respond_to? :should
field_checked.should be_false
else
assert !field_checked
end
end
end
Then /^(?:|I )should be on (.+)$/ do |page_name|
current_path = URI.parse(current_url).path
if current_path.respond_to? :should
current_path.should == path_to(page_name)
else
assert_equal path_to(page_name), current_path
end
end
Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
query = URI.parse(current_url).query
actual_params = query ? CGI.parse(query) : {}
expected_params = {}
expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')}
if actual_params.respond_to? :should
actual_params.should == expected_params
else
assert_equal expected_params, actual_params
end
end
Then /^I save and open the page$/ do
save_and_open_page
end
Then /^show me the page$/ do
save_and_open_page
end
require 'capybara/cucumber'
require 'test/unit/assertions'
World(Test::Unit::Assertions)
module NavigationHelpers
# Maps a name to a path. Used by the
#
# When /^I go to (.+)$/ do |page_name|
#
# step definition in web_steps.rb
#
def path_to(page_name)
case page_name
when /the new user page/
'/users/new'
when /the home\s?page/
'/'
# Add more mappings here.
# 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
begin
page_name =~ /the (.*) page/
path_components = $1.split(/\s+/)
self.send(path_components.push('path').join('_').to_sym)
rescue Object => e
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
end
World(NavigationHelpers)
PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze
TEMP_ROOT = File.join(PROJECT_ROOT, 'tmp').freeze
APP_NAME = 'testapp'.freeze
CUC_RAILS_ROOT = File.join(TEMP_ROOT, APP_NAME).freeze
ENV['RAILS_ENV'] = 'test'
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)
# This file was generated by Appraisal
source "http://rubygems.org"
gem "activerecord", :require=>"active_record"
gem "appraisal"
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 "shoulda"
gem "sqlite3", "~>1.3.4"
gem "sprockets", "~> 2.0.0.beta.13", :require=>false
gem "rails", "~> 2.3.12"
GEM
remote: http://rubygems.org/
specs:
actionmailer (2.3.12)
actionpack (= 2.3.12)
actionpack (2.3.12)
activesupport (= 2.3.12)
rack (~> 1.1.0)
activerecord (2.3.12)
activesupport (= 2.3.12)
activeresource (2.3.12)
activesupport (= 2.3.12)
activesupport (2.3.12)
appraisal (0.3.7)
aruba (~> 0.4.2)
bundler
rake
aruba (0.4.3)
bcat (>= 0.6.1)
childprocess (>= 0.1.9)
cucumber (>= 0.10.7)
rdiscount (>= 1.6.8)
rspec (>= 2.6.0)
aws-s3 (0.6.2)
builder
mime-types
xml-simple
bcat (0.6.1)
rack (~> 1.0)
bouncy-castle-java (1.5.0146.1)
builder (3.0.0)
childprocess (0.2.0)
ffi (~> 1.0.6)
cocaine (0.2.0)
cucumber (1.0.2)
builder (>= 2.1.2)
diff-lcs (>= 1.1.2)
gherkin (~> 2.4.5)
json (>= 1.4.6)
term-ansicolor (>= 1.0.5)
diff-lcs (1.1.2)
excon (0.6.5)
ffi (1.0.9)
ffi (1.0.9-java)
fog (0.9.0)
builder
excon (~> 0.6.1)
formatador (>= 0.1.3)
json
mime-types
net-scp (>= 1.0.4)
net-ssh (>= 2.1.4)
nokogiri (>= 1.4.4)
ruby-hmac
formatador (0.2.0)
gherkin (2.4.5)
json (>= 1.4.6)
gherkin (2.4.5-java)
json (>= 1.4.6)
hike (1.2.0)
jruby-openssl (0.7.4)
bouncy-castle-java
json (1.5.3)
json (1.5.3-java)
mime-types (1.16)
mocha (0.9.12)
net-scp (1.0.4)
net-ssh (>= 1.99.1)
net-ssh (2.1.4)
nokogiri (1.5.0)
nokogiri (1.5.0-java)
rack (1.1.2)
rails (2.3.12)
actionmailer (= 2.3.12)
actionpack (= 2.3.12)
activerecord (= 2.3.12)
activeresource (= 2.3.12)
activesupport (= 2.3.12)
rake (>= 0.8.3)
rake (0.9.2)
rdiscount (1.6.8)
rdoc (3.8)
rspec (2.6.0)
rspec-core (~> 2.6.0)
rspec-expectations (~> 2.6.0)
rspec-mocks (~> 2.6.0)
rspec-core (2.6.4)
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
ruby-hmac (0.4.0)
shoulda (2.11.3)
sprockets (2.0.0.beta.13)
hike (~> 1.2)
rack (~> 1.0)
tilt (!= 1.3.0, ~> 1.1)
sqlite3 (1.3.4)
term-ansicolor (1.0.6)
tilt (1.3.2)
xml-simple (1.1.0)
PLATFORMS
java
ruby
DEPENDENCIES
activerecord
appraisal
aws-s3
bundler
cocaine (~> 0.2)
fog
jruby-openssl
mime-types
mocha
rails (~> 2.3.12)
rake
rdoc
shoulda
sprockets (~> 2.0.0.beta.13)
sqlite3 (~> 1.3.4)
# This file was generated by Appraisal
source "http://rubygems.org"
gem "activerecord", :require=>"active_record"
gem "appraisal"
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 "shoulda"
gem "sqlite3", "~>1.3.4"
gem "sprockets", "~> 2.0.0.beta.13", :require=>false
gem "rails", "~> 3.0.9"
GEM
remote: http://rubygems.org/
specs:
abstract (1.0.0)
actionmailer (3.0.9)
actionpack (= 3.0.9)
mail (~> 2.2.19)
actionpack (3.0.9)
activemodel (= 3.0.9)
activesupport (= 3.0.9)
builder (~> 2.1.2)
erubis (~> 2.6.6)
i18n (~> 0.5.0)
rack (~> 1.2.1)
rack-mount (~> 0.6.14)
rack-test (~> 0.5.7)
tzinfo (~> 0.3.23)
activemodel (3.0.9)
activesupport (= 3.0.9)
builder (~> 2.1.2)
i18n (~> 0.5.0)
activerecord (3.0.9)
activemodel (= 3.0.9)
activesupport (= 3.0.9)
arel (~> 2.0.10)
tzinfo (~> 0.3.23)
activeresource (3.0.9)
activemodel (= 3.0.9)
activesupport (= 3.0.9)
activesupport (3.0.9)
appraisal (0.3.7)
aruba (~> 0.4.2)
bundler
rake
arel (2.0.10)
aruba (0.4.3)
bcat (>= 0.6.1)
childprocess (>= 0.1.9)
cucumber (>= 0.10.7)
rdiscount (>= 1.6.8)
rspec (>= 2.6.0)
aws-s3 (0.6.2)
builder
mime-types
xml-simple
bcat (0.6.1)
rack (~> 1.0)
bouncy-castle-java (1.5.0146.1)
builder (2.1.2)
childprocess (0.2.0)
ffi (~> 1.0.6)
cocaine (0.2.0)
cucumber (1.0.2)
builder (>= 2.1.2)
diff-lcs (>= 1.1.2)
gherkin (~> 2.4.5)
json (>= 1.4.6)
term-ansicolor (>= 1.0.5)
diff-lcs (1.1.2)
erubis (2.6.6)
abstract (>= 1.0.0)
excon (0.6.5)
ffi (1.0.9)
ffi (1.0.9-java)
fog (0.9.0)
builder
excon (~> 0.6.1)
formatador (>= 0.1.3)
json
mime-types
net-scp (>= 1.0.4)
net-ssh (>= 2.1.4)
nokogiri (>= 1.4.4)
ruby-hmac
formatador (0.2.0)
gherkin (2.4.5)
json (>= 1.4.6)
gherkin (2.4.5-java)
json (>= 1.4.6)
hike (1.2.0)
i18n (0.5.0)
jruby-openssl (0.7.4)
bouncy-castle-java
json (1.5.3)
json (1.5.3-java)
mail (2.2.19)
activesupport (>= 2.3.6)
i18n (>= 0.4.0)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.16)
mocha (0.9.12)
net-scp (1.0.4)
net-ssh (>= 1.99.1)
net-ssh (2.1.4)
nokogiri (1.5.0)
nokogiri (1.5.0-java)
polyglot (0.3.1)
rack (1.2.3)
rack-mount (0.6.14)
rack (>= 1.0.0)
rack-test (0.5.7)
rack (>= 1.0)
rails (3.0.9)
actionmailer (= 3.0.9)
actionpack (= 3.0.9)
activerecord (= 3.0.9)
activeresource (= 3.0.9)
activesupport (= 3.0.9)
bundler (~> 1.0)
railties (= 3.0.9)
railties (3.0.9)
actionpack (= 3.0.9)
activesupport (= 3.0.9)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (~> 0.14.4)
rake (0.9.2)
rdiscount (1.6.8)
rdoc (3.8)
rspec (2.6.0)
rspec-core (~> 2.6.0)
rspec-expectations (~> 2.6.0)
rspec-mocks (~> 2.6.0)
rspec-core (2.6.4)
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
ruby-hmac (0.4.0)
shoulda (2.11.3)
sprockets (2.0.0.beta.13)
hike (~> 1.2)
rack (~> 1.0)
tilt (!= 1.3.0, ~> 1.1)
sqlite3 (1.3.4)
term-ansicolor (1.0.6)
thor (0.14.6)
tilt (1.3.2)
treetop (1.4.9)
polyglot (>= 0.3.1)
tzinfo (0.3.29)
xml-simple (1.1.0)
PLATFORMS
java
ruby
DEPENDENCIES
activerecord
appraisal
aws-s3
bundler
cocaine (~> 0.2)
fog
jruby-openssl
mime-types
mocha
rails (~> 3.0.9)
rake
rdoc
shoulda
sprockets (~> 2.0.0.beta.13)
sqlite3 (~> 1.3.4)
# This file was generated by Appraisal
source "http://rubygems.org"
gem "activerecord", :require=>"active_record"
gem "appraisal"
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 "shoulda"
gem "sqlite3", "~>1.3.4"
gem "sprockets", "~> 2.0.0.beta.13", :require=>false
gem "rails", "~> 3.1.0.rc5"
GEM
remote: http://rubygems.org/
specs:
actionmailer (3.1.0.rc6)
actionpack (= 3.1.0.rc6)
mail (~> 2.3.0)
actionpack (3.1.0.rc6)
activemodel (= 3.1.0.rc6)
activesupport (= 3.1.0.rc6)
builder (~> 3.0.0)
erubis (~> 2.7.0)
i18n (~> 0.6)
rack (~> 1.3.2)
rack-cache (~> 1.0.2)
rack-mount (~> 0.8.1)
rack-test (~> 0.6.0)
sprockets (~> 2.0.0.beta.12)
activemodel (3.1.0.rc6)
activesupport (= 3.1.0.rc6)
bcrypt-ruby (~> 2.1.4)
builder (~> 3.0.0)
i18n (~> 0.6)
activerecord (3.1.0.rc6)
activemodel (= 3.1.0.rc6)
activesupport (= 3.1.0.rc6)
arel (~> 2.2.1)
tzinfo (~> 0.3.29)
activeresource (3.1.0.rc6)
activemodel (= 3.1.0.rc6)
activesupport (= 3.1.0.rc6)
activesupport (3.1.0.rc6)
multi_json (~> 1.0)
appraisal (0.3.7)
aruba (~> 0.4.2)
bundler
rake
arel (2.2.1)
aruba (0.4.5)
bcat (>= 0.6.1)
childprocess (>= 0.1.9)
cucumber (>= 0.10.7)
rdiscount (>= 1.6.8)
rspec (>= 2.6.0)
aws-s3 (0.6.2)
builder
mime-types
xml-simple
bcat (0.6.1)
rack (~> 1.0)
bcrypt-ruby (2.1.4)
bcrypt-ruby (2.1.4-java)
bouncy-castle-java (1.5.0146.1)
builder (3.0.0)
childprocess (0.2.0)
ffi (~> 1.0.6)
cocaine (0.2.0)
cucumber (1.0.2)
builder (>= 2.1.2)
diff-lcs (>= 1.1.2)
gherkin (~> 2.4.5)
json (>= 1.4.6)
term-ansicolor (>= 1.0.5)
diff-lcs (1.1.2)
erubis (2.7.0)
excon (0.6.5)
ffi (1.0.9)
ffi (1.0.9-java)
fog (0.10.0)
builder
excon (~> 0.6.5)
formatador (~> 0.2.0)
mime-types
multi_json (~> 1.0.3)
net-scp (~> 1.0.4)
net-ssh (~> 2.1.4)
nokogiri (~> 1.5.0)
ruby-hmac
formatador (0.2.0)
gherkin (2.4.5)
json (>= 1.4.6)
gherkin (2.4.5-java)
json (>= 1.4.6)
hike (1.2.1)
i18n (0.6.0)
jruby-openssl (0.7.4)
bouncy-castle-java
json (1.5.3)
json (1.5.3-java)
mail (2.3.0)
i18n (>= 0.4.0)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.16)
mocha (0.9.12)
multi_json (1.0.3)
net-scp (1.0.4)
net-ssh (>= 1.99.1)
net-ssh (2.1.4)
nokogiri (1.5.0)
nokogiri (1.5.0-java)
polyglot (0.3.2)
rack (1.3.2)
rack-cache (1.0.2)
rack (>= 0.4)
rack-mount (0.8.2)
rack (>= 1.0.0)
rack-ssl (1.3.2)
rack
rack-test (0.6.1)
rack (>= 1.0)
rails (3.1.0.rc6)
actionmailer (= 3.1.0.rc6)
actionpack (= 3.1.0.rc6)
activerecord (= 3.1.0.rc6)
activeresource (= 3.1.0.rc6)
activesupport (= 3.1.0.rc6)
bundler (~> 1.0)
railties (= 3.1.0.rc6)
railties (3.1.0.rc6)
actionpack (= 3.1.0.rc6)
activesupport (= 3.1.0.rc6)
rack-ssl (~> 1.3.2)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (~> 0.14.6)
rake (0.9.2)
rdiscount (1.6.8)
rdoc (3.9.2)
rspec (2.6.0)
rspec-core (~> 2.6.0)
rspec-expectations (~> 2.6.0)
rspec-mocks (~> 2.6.0)
rspec-core (2.6.4)
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
ruby-hmac (0.4.0)
shoulda (2.11.3)
sprockets (2.0.0.beta.13)
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.4)
term-ansicolor (1.0.6)
thor (0.14.6)
tilt (1.3.2)
treetop (1.4.10)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.29)
xml-simple (1.1.0)
PLATFORMS
java
ruby
DEPENDENCIES
activerecord
appraisal
aws-s3
bundler
cocaine (~> 0.2)
fog
jruby-openssl
mime-types
mocha
rails (~> 3.1.0.rc5)
rake
rdoc
shoulda
sprockets (~> 2.0.0.beta.13)
sqlite3 (~> 1.3.4)
require File.join(File.dirname(__FILE__), "lib", "paperclip") require File.join(File.dirname(__FILE__), "lib", "paperclip")
require 'paperclip/railtie'
Paperclip::Railtie.insert
Description:
Explain the generator
Example:
rails generate paperclip Thing
This will create:
what/will/it/create
require 'rails/generators/active_record'
class PaperclipGenerator < ActiveRecord::Generators::Base
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.",
:banner => "attachment_one attachment_two attachment_three ..."
def self.source_root
@source_root ||= File.expand_path('../templates', __FILE__)
end
def generate_migration
migration_template "paperclip_migration.rb.erb", "db/migrate/#{migration_file_name}"
end
protected
def migration_name
"add_attachment_#{attachment_names.join("_")}_to_#{name.underscore}"
end
def migration_file_name
"#{migration_name}.rb"
end
def migration_class_name
migration_name.camelize
end
end
class <%= migration_class_name %> < ActiveRecord::Migration
def self.up
<% attachment_names.each do |attachment| -%>
add_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_file_name, :string
add_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_content_type, :string
add_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_file_size, :integer
add_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_updated_at, :datetime
<% end -%>
end
def self.down
<% attachment_names.each do |attachment| -%>
remove_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_file_name
remove_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_content_type
remove_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_file_size
remove_column :<%= name.underscore.camelize.tableize %>, :<%= attachment %>_updated_at
<% end -%>
end
end
...@@ -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
...@@ -26,28 +26,27 @@ ...@@ -26,28 +26,27 @@
# See the +has_attached_file+ documentation for more details. # See the +has_attached_file+ documentation for more details.
require 'erb' require 'erb'
require 'digest'
require 'tempfile' require 'tempfile'
require 'paperclip/version'
require 'paperclip/upfile' require 'paperclip/upfile'
require 'paperclip/iostream' require 'paperclip/iostream'
require 'paperclip/geometry' require 'paperclip/geometry'
require 'paperclip/processor' require 'paperclip/processor'
require 'paperclip/thumbnail' require 'paperclip/thumbnail'
require 'paperclip/storage'
require 'paperclip/interpolations' require 'paperclip/interpolations'
require 'paperclip/style' require 'paperclip/style'
require 'paperclip/attachment' require 'paperclip/attachment'
if defined? RAILS_ROOT require 'paperclip/storage'
Dir.glob(File.join(File.expand_path(RAILS_ROOT), "lib", "paperclip_processors", "*.rb")).each do |processor| require 'paperclip/callback_compatibility'
require processor require 'paperclip/railtie'
end require 'logger'
end require 'cocaine'
# 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.
module Paperclip module Paperclip
VERSION = "2.3.1.1"
class << self class << self
# Provides configurability to Paperclip. There are a number of options available, such as: # Provides configurability to Paperclip. There are a number of options available, such as:
# * whiny: Will raise an error if Paperclip cannot process thumbnails of # * whiny: Will raise an error if Paperclip cannot process thumbnails of
...@@ -64,60 +63,46 @@ module Paperclip ...@@ -64,60 +63,46 @@ module Paperclip
:image_magick_path => nil, :image_magick_path => nil,
:command_path => nil, :command_path => nil,
:log => true, :log => true,
:log_command => false, :log_command => true,
:swallow_stderr => true :swallow_stderr => true
} }
end end
def path_for_command command #:nodoc: def configure
if options[:image_magick_path] yield(self) if block_given?
warn("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
end
path = [options[:command_path] || options[:image_magick_path], command].compact
File.join(*path)
end end
def interpolates key, &block def interpolates key, &block
Paperclip::Interpolations[key] = block Paperclip::Interpolations[key] = block
end end
# The run method takes a command to execute and a string of parameters # The run method takes a command to execute and an array of parameters
# that get passed to it. The command is prefixed with the :command_path # that get passed to it. The command is prefixed with the :command_path
# option from Paperclip.options. If you have many commands to run and # option from Paperclip.options. If you have many commands to run and
# they are in different paths, the suggested course of action is to # they are in different paths, the suggested course of action is to
# symlink them so they are all in the same directory. # symlink them so they are all in the same directory.
# #
# If the command returns with a result code that is not one of the # If the command returns with a result code that is not one of the
# expected_outcodes, a PaperclipCommandLineError will be raised. Generally # expected_outcodes, a Cocaine::CommandLineError will be raised. Generally
# a code of 0 is expected, but a list of codes may be passed if necessary. # 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])
# #
# This method can log the command being run when # This method can log the command being run when
# Paperclip.options[:log_command] is set to true (defaults to false). This # 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. # will only log if logging in general is set to true as well.
def run cmd, params = "", expected_outcodes = 0 def run cmd, *params
command = %Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ") if options[:image_magick_path]
command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr] Paperclip.log("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
Paperclip.log(command) if Paperclip.options[:log_command]
output = `#{command}`
unless [expected_outcodes].flatten.include?($?.exitstatus)
raise PaperclipCommandLineError, "Error while running #{cmd}"
end
output
end
def bit_bucket #:nodoc:
File.exists?("/dev/null") ? "/dev/null" : "NUL"
end
def included base #:nodoc:
base.extend ClassMethods
unless base.respond_to?(:define_callbacks)
base.send(:include, Paperclip::CallbackCompatability)
end end
Cocaine::CommandLine.path = options[:command_path] || options[:image_magick_path]
Cocaine::CommandLine.new(cmd, *params).run
end end
def processor name #:nodoc: def processor name #:nodoc:
name = name.to_s.camelize name = name.to_s.camelize
load_processor(name) unless Paperclip.const_defined?(name)
processor = Paperclip.const_get(name) processor = Paperclip.const_get(name)
unless processor.ancestors.include?(Paperclip::Processor) unless processor.ancestors.include?(Paperclip::Processor)
raise PaperclipError.new("Processor #{name} was not found") raise PaperclipError.new("Processor #{name} was not found")
...@@ -125,6 +110,18 @@ module Paperclip ...@@ -125,6 +110,18 @@ module Paperclip
processor processor
end end
def load_processor(name)
if defined?(Rails.root) && Rails.root
require File.expand_path(Rails.root.join("lib", "paperclip_processors", "#{name.underscore}.rb"))
end
end
def each_instance_with_attachment(klass, name)
class_for(klass).all.each do |instance|
yield(instance) if instance.send(:"#{name}?")
end
end
# Log a paperclip-specific line. Uses ActiveRecord::Base.logger # Log a paperclip-specific line. Uses ActiveRecord::Base.logger
# 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
...@@ -132,18 +129,50 @@ module Paperclip ...@@ -132,18 +129,50 @@ module Paperclip
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
def class_for(class_name)
# 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
end end
class PaperclipError < StandardError #:nodoc: class PaperclipError < StandardError #:nodoc:
end end
class PaperclipCommandLineError < StandardError #:nodoc: class StorageMethodNotFound < PaperclipError
end
class CommandNotFoundError < PaperclipError
end end
class NotIdentifiedByImageMagickError < PaperclipError #:nodoc: class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
...@@ -152,6 +181,18 @@ module Paperclip ...@@ -152,6 +181,18 @@ module Paperclip
class InfiniteInterpolationError < PaperclipError #:nodoc: class InfiniteInterpolationError < PaperclipError #:nodoc:
end end
module Glue
def self.included base #:nodoc:
base.extend ClassMethods
base.class_attribute :attachment_definitions if base.respond_to?(:class_attribute)
if base.respond_to?(:set_callback)
base.send :include, Paperclip::CallbackCompatability::Rails3
else
base.send :include, Paperclip::CallbackCompatability::Rails21
end
end
end
module ClassMethods module ClassMethods
# +has_attached_file+ gives the class it is called on an attribute that maps to a file. This # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
# is typically a file stored somewhere on the filesystem and has been uploaded by a user. # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
...@@ -192,7 +233,7 @@ module Paperclip ...@@ -192,7 +233,7 @@ module Paperclip
# Defaults to true. This option used to be called :whiny_thumbanils, but this is # Defaults to true. This option used to be called :whiny_thumbanils, but this is
# deprecated. # deprecated.
# * +convert_options+: When creating thumbnails, use this free-form options # * +convert_options+: When creating thumbnails, use this free-form options
# field to pass in various convert command options. Typical options are "-strip" to # array to pass in various convert command options. Typical options are "-strip" to
# remove all Exif data from the image (save space for thumbnails and avatars) or # remove all Exif data from the image (save space for thumbnails and avatars) or
# "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
# convert documentation for more options: (http://www.imagemagick.org/script/convert.php) # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
...@@ -208,7 +249,10 @@ module Paperclip ...@@ -208,7 +249,10 @@ 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
# shell quoting for safety. If your options require a space, please pre-split them
# and pass an array to :convert_options instead.
# * +storage+: Chooses the storage backend where the files will be stored. The current # * +storage+: Chooses the storage backend where the files will be stored. The current
# 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
...@@ -216,14 +260,20 @@ module Paperclip ...@@ -216,14 +260,20 @@ module Paperclip
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)
after_save :save_attached_files after_save :save_attached_files
before_destroy :destroy_attached_files before_destroy :destroy_attached_files
define_callbacks :before_post_process, :after_post_process define_paperclip_callbacks :post_process, :"#{name}_post_process"
define_callbacks :"before_#{name}_post_process", :"after_#{name}_post_process"
define_method name do |*args| define_method name do |*args|
a = attachment_for(name) a = attachment_for(name)
...@@ -257,14 +307,16 @@ module Paperclip ...@@ -257,14 +307,16 @@ 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.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s) message = message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
validates_inclusion_of :"#{name}_file_size", validates_inclusion_of :"#{name}_file_size",
:in => range, :in => range,
:message => message, :message => message,
:if => options[:if], :if => options[:if],
:unless => options[:unless] :unless => options[:unless],
:allow_nil => true
end end
# Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true. # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
...@@ -281,7 +333,7 @@ module Paperclip ...@@ -281,7 +333,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],
...@@ -304,21 +356,34 @@ module Paperclip ...@@ -304,21 +356,34 @@ 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 = {}
types = [options.delete(:content_type)].flatten validation_options = options.dup
validates_each(:"#{name}_content_type", options) do |record, attr, value| allowed_types = [validation_options[:content_type]].flatten
unless types.any?{|t| t === value } validates_each(:"#{name}_content_type", validation_options) do |record, attr, value|
if !allowed_types.any?{|t| t === value } && !(value.nil? || value.blank?)
if record.errors.method(:add).arity == -2
message = options[:message] || "is not one of #{allowed_types.join(", ")}"
message = message.call if message.respond_to?(:call)
record.errors.add(:"#{name}_content_type", message)
else
record.errors.add(:"#{name}_content_type", :inclusion, :default => options[:message], :value => value) record.errors.add(:"#{name}_content_type", :inclusion, :default => options[:message], :value => value)
end end
end end
end end
end
# 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
if respond_to?(:class_attribute)
self.attachment_definitions
else
read_inheritable_attribute(:attachment_definitions) read_inheritable_attribute(:attachment_definitions)
end end
end end
end
module InstanceMethods #:nodoc: module InstanceMethods #:nodoc:
def attachment_for name def attachment_for name
...@@ -333,14 +398,14 @@ module Paperclip ...@@ -333,14 +398,14 @@ module Paperclip
end end
def save_attached_files def save_attached_files
logger.info("[paperclip] Saving attachments.") Paperclip.log("Saving attachments.")
each_attachment do |name, attachment| each_attachment do |name, attachment|
attachment.send(:save) attachment.send(:save)
end end
end end
def destroy_attached_files def destroy_attached_files
logger.info("[paperclip] Deleting attachments.") Paperclip.log("Deleting attachments.")
each_attachment do |name, attachment| each_attachment do |name, attachment|
attachment.send(:queue_existing_for_delete) attachment.send(:queue_existing_for_delete)
attachment.send(:flush_deletes) attachment.send(:flush_deletes)
...@@ -349,9 +414,3 @@ module Paperclip ...@@ -349,9 +414,3 @@ module Paperclip
end end
end end
# Set it all up.
if Object.const_defined?("ActiveRecord")
ActiveRecord::Base.send(:include, Paperclip)
File.send(:include, Paperclip::Upfile)
end
...@@ -4,22 +4,31 @@ module Paperclip ...@@ -4,22 +4,31 @@ module Paperclip
# when the model saves, deletes when the model is destroyed, and processes # when the model saves, deletes when the model is destroyed, and processes
# the file upon assignment. # the file upon assignment.
class Attachment class Attachment
include IOStream
def self.default_options def self.default_options
@default_options ||= { @default_options ||= {
: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,
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails] :use_timestamp => true,
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
:use_default_time_zone => true,
:hash_digest => "SHA1",
: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, :source_file_options
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
...@@ -35,14 +44,23 @@ module Paperclip ...@@ -35,14 +44,23 @@ module Paperclip
@path = options[:path] @path = options[:path]
@path = @path.call(self) if @path.is_a?(Proc) @path = @path.call(self) if @path.is_a?(Proc)
@styles = options[:styles] @styles = options[:styles]
@only_process = options[:only_process]
@normalized_styles = nil @normalized_styles = nil
@default_url = options[:default_url] @default_url = options[:default_url]
@default_style = options[:default_style] @default_style = options[:default_style]
@storage = options[:storage] @storage = options[:storage]
@use_timestamp = options[:use_timestamp]
@whiny = options[:whiny_thumbnails] || options[:whiny] @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] @convert_options = options[:convert_options]
@source_file_options = options[:source_file_options]
@processors = options[:processors] @processors = options[:processors]
@preserve_files = options[:preserve_files]
@options = options @options = options
@post_processing = true
@queued_for_delete = [] @queued_for_delete = []
@queued_for_write = {} @queued_for_write = {}
@errors = {} @errors = {}
...@@ -52,10 +70,10 @@ module Paperclip ...@@ -52,10 +70,10 @@ module Paperclip
end end
def styles def styles
unless @normalized_styles if @styles.respond_to?(:call) || !@normalized_styles
@normalized_styles = {} @normalized_styles = ActiveSupport::OrderedHash.new
(@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args| (@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args|
@normalized_styles[name] = Paperclip::Style.new(name, args, self) @normalized_styles[name] = Paperclip::Style.new(name, args.dup, self)
end end
end end
@normalized_styles @normalized_styles
...@@ -86,18 +104,20 @@ module Paperclip ...@@ -86,18 +104,20 @@ module Paperclip
return nil if uploaded_file.nil? return nil if uploaded_file.nil?
@queued_for_write[:original] = uploaded_file.to_tempfile @queued_for_write[:original] = to_tempfile(uploaded_file)
instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^A-Za-z\d\.\-_]+/, '_')) instance_write(:file_name, uploaded_file.original_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(:updated_at, Time.now) instance_write(:updated_at, Time.now)
@dirty = true @dirty = true
post_process post_process(*@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)
instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
ensure ensure
uploaded_file.close if close_uploaded_file uploaded_file.close if close_uploaded_file
end end
...@@ -106,12 +126,12 @@ module Paperclip ...@@ -106,12 +126,12 @@ module Paperclip
# this does not necessarily need to point to a file that your web server # this does not necessarily need to point to a file that your web server
# can access and can point to an action in your app, if you need fine # can access and can point to an action in your app, if you need fine
# 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 # security, however, for performance reasons. Set use_timestamp to false
# include_updated_timestamp to false if you want to stop the attachment # if you want to stop the attachment update time appended to the url
# update time appended to the url def url(style_name = default_style, use_timestamp = @use_timestamp)
def url style_name = default_style, include_updated_timestamp = true default_url = @default_url.is_a?(Proc) ? @default_url.call(self) : @default_url
url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name) url = original_filename.nil? ? interpolate(default_url, style_name) : interpolate(@url, style_name)
include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url use_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : 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
...@@ -123,7 +143,7 @@ module Paperclip ...@@ -123,7 +143,7 @@ module Paperclip
end end
# Alias to +url+ # Alias to +url+
def to_s style_name = nil def to_s style_name = default_style
url(style_name) url(style_name)
end end
...@@ -151,6 +171,7 @@ module Paperclip ...@@ -151,6 +171,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
...@@ -158,9 +179,11 @@ module Paperclip ...@@ -158,9 +179,11 @@ 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
unless @preserve_files
clear clear
save save
end end
end
# Returns the name of the file as originally assigned, and lives in the # Returns the name of the file as originally assigned, and lives in the
# <attachment>_file_name attribute of the model. # <attachment>_file_name attribute of the model.
...@@ -174,6 +197,12 @@ module Paperclip ...@@ -174,6 +197,12 @@ module Paperclip
instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size) instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
end end
# Returns the hash of the file as originally assigned, and lives in the
# <attachment>_fingerprint attribute of the model.
def fingerprint
instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
end
# Returns the content_type of the file as originally assigned, and lives # Returns the content_type of the file as originally assigned, and lives
# in the <attachment>_content_type attribute of the model. # in the <attachment>_content_type attribute of the model.
def content_type def content_type
...@@ -187,6 +216,31 @@ module Paperclip ...@@ -187,6 +216,31 @@ module Paperclip
time && time.to_f.to_i time && time.to_f.to_i
end end
# The time zone to use for timestamp interpolation. Using the default
# time zone ensures that results are consistent across all threads.
def time_zone
@use_default_time_zone ? Time.zone_default : Time.zone
end
# Returns a unique hash suitable for obfuscating the URL of an otherwise
# publicly viewable attachment.
def hash(style_name = default_style)
raise ArgumentError, "Unable to generate hash without :hash_secret" unless @hash_secret
require 'openssl' unless defined?(OpenSSL)
data = interpolate(@hash_data, style_name)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@hash_digest).new, @hash_secret, data)
end
def generate_fingerprint(source)
if source.respond_to?(:path) && source.path && !source.path.blank?
Digest::MD5.file(source.path).to_s
else
data = source.read
source.rewind if source.respond_to?(:rewind)
Digest::MD5.hexdigest(data)
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
# to vary the storage location based on name, id, style, class, etc. # to vary the storage location based on name, id, style, class, etc.
# This method is a deprecated access into supplying and retrieving these # This method is a deprecated access into supplying and retrieving these
...@@ -203,15 +257,16 @@ module Paperclip ...@@ -203,15 +257,16 @@ module Paperclip
# in the paperclip:refresh rake task and that's it. It will regenerate all # in the paperclip:refresh rake task and that's it. It will regenerate all
# thumbnails forcefully, by reobtaining the original file and going through # thumbnails forcefully, by reobtaining the original file and going through
# the post-process again. # the post-process again.
def reprocess! def reprocess!(*style_args)
new_original = Tempfile.new("paperclip-reprocess") new_original = Tempfile.new("paperclip-reprocess")
new_original.binmode new_original.binmode
if old_original = to_file(:original) if old_original = to_file(:original)
new_original.write( old_original.read ) new_original.write( old_original.respond_to?(:get) ? old_original.get : old_original.read )
new_original.rewind new_original.rewind
@queued_for_write = { :original => new_original } @queued_for_write = { :original => new_original }
post_process instance_write(:updated_at, Time.now)
post_process(*style_args)
old_original.close if old_original.respond_to?(:close) old_original.close if old_original.respond_to?(:close)
...@@ -219,6 +274,9 @@ module Paperclip ...@@ -219,6 +274,9 @@ module Paperclip
else else
true true
end end
rescue Errno::EACCES => e
warn "#{e} - skipping file"
false
end end
# Returns true if a file has been assigned. # Returns true if a file has been assigned.
...@@ -226,6 +284,8 @@ module Paperclip ...@@ -226,6 +284,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).
...@@ -265,7 +325,12 @@ module Paperclip ...@@ -265,7 +325,12 @@ module Paperclip
end end
def initialize_storage #:nodoc: def initialize_storage #:nodoc:
@storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize) storage_class_name = @storage.to_s.downcase.camelize
begin
@storage_module = Paperclip::Storage.const_get(storage_class_name)
rescue NameError
raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
end
self.extend(@storage_module) self.extend(@storage_module)
end end
...@@ -278,29 +343,33 @@ module Paperclip ...@@ -278,29 +343,33 @@ module Paperclip
[ style_options, all_options ].compact.join(" ") [ style_options, all_options ].compact.join(" ")
end end
def post_process #:nodoc: def extra_source_file_options_for(style) #:nodoc:
return if @queued_for_write[:original].nil? all_options = source_file_options[:all]
return if fire_events(:before) all_options = all_options.call(instance) if all_options.respond_to?(:call)
post_process_styles style_options = source_file_options[style]
return if fire_events(:after) style_options = style_options.call(instance) if style_options.respond_to?(:call)
end
def fire_events(which) #:nodoc: [ style_options, all_options ].compact.join(" ")
return true if callback(:"#{which}_post_process") == false
return true if callback(:"#{which}_#{name}_post_process") == false
end end
def callback which #:nodoc: def post_process(*style_args) #:nodoc:
instance.run_callbacks(which, @queued_for_write){|result, obj| result == false } return if @queued_for_write[:original].nil?
instance.run_paperclip_callbacks(:post_process) do
instance.run_paperclip_callbacks(:"#{name}_post_process") do
post_process_styles(*style_args)
end
end
end end
def post_process_styles #:nodoc: def post_process_styles(*style_args) #:nodoc:
styles.each do |name, style| styles.each do |name, style|
begin begin
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?
@queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor| @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
Paperclip.processor(processor).make(file, style.processor_options, self) Paperclip.processor(processor).make(file, style.processor_options, self)
end 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 @whiny
...@@ -313,7 +382,7 @@ module Paperclip ...@@ -313,7 +382,7 @@ module Paperclip
end end
def queue_existing_for_delete #:nodoc: def queue_existing_for_delete #:nodoc:
return unless file? return unless (file? && @preserve_files==false)
@queued_for_delete += [:original, *styles.keys].uniq.map do |style| @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
path(style) if exists?(style) path(style) if exists?(style)
end.compact end.compact
...@@ -331,4 +400,3 @@ module Paperclip ...@@ -331,4 +400,3 @@ module Paperclip
end end
end end
module Paperclip
# This module is intended as a compatability shim for the differences in
# callbacks between Rails 2.0 and Rails 2.1.
module CallbackCompatability
def self.included(base)
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
end
module ClassMethods
# The implementation of this method is taken from the Rails 1.2.6 source,
# from rails/activerecord/lib/active_record/callbacks.rb, line 192.
def define_callbacks(*args)
args.each do |method|
self.class_eval <<-"end_eval"
def self.#{method}(*callbacks, &block)
callbacks << block if block_given?
write_inheritable_array(#{method.to_sym.inspect}, callbacks)
end
end_eval
end
end
end
module InstanceMethods
# The callbacks in < 2.1 don't worry about the extra options or the
# block, so just run what we have available.
def run_callbacks(meth, opts = nil, &blk)
callback(meth)
end
end
end
end
module Paperclip
module CallbackCompatability
module Rails21
def self.included(base)
base.extend(Defining)
base.send(:include, Running)
end
module Defining
def define_paperclip_callbacks(*args)
args.each do |callback|
define_callbacks("before_#{callback}")
define_callbacks("after_#{callback}")
end
end
end
module Running
def run_paperclip_callbacks(callback, opts = nil, &blk)
# The overall structure of this isn't ideal since after callbacks run even if
# befores return false. But this is how rails 3's callbacks work, unfortunately.
if run_callbacks(:"before_#{callback}"){ |result, object| result == false } != false
blk.call
end
run_callbacks(:"after_#{callback}"){ |result, object| result == false }
end
end
end
module Rails3
def self.included(base)
base.extend(Defining)
base.send(:include, Running)
end
module Defining
def define_paperclip_callbacks(*callbacks)
define_callbacks *[callbacks, {:terminator => "result == false"}].flatten
callbacks.each do |callback|
eval <<-end_callbacks
def before_#{callback}(*args, &blk)
set_callback(:#{callback}, :before, *args, &blk)
end
def after_#{callback}(*args, &blk)
set_callback(:#{callback}, :after, *args, &blk)
end
end_callbacks
end
end
end
module Running
def run_paperclip_callbacks(callback, opts = nil, &block)
run_callbacks(callback, opts, &block)
end
end
end
end
end
...@@ -15,10 +15,13 @@ module Paperclip ...@@ -15,10 +15,13 @@ module Paperclip
# File or path. # File or path.
def self.from_file file def self.from_file file
file = file.path if file.respond_to? "path" file = file.path if file.respond_to? "path"
raise(Paperclip::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")) if file.blank?
geometry = begin geometry = begin
Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]]) Paperclip.run("identify", "-format %wx%h :file", :file => "#{file}[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} is not recognized by the 'identify' command."))
......
...@@ -6,13 +6,13 @@ module Paperclip ...@@ -6,13 +6,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)
...@@ -35,30 +35,41 @@ module Paperclip ...@@ -35,30 +35,41 @@ module Paperclip
# 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
# contains ":url" to prevent infinite recursion. This interpolation # contains ":url" to prevent infinite recursion. This interpolation
# is used in the default :path to ease default specifications. # is used in the default :path to ease default specifications.
RIGHT_HERE = "#{__FILE__.gsub(%r{^\./}, "")}:#{__LINE__ + 3}"
def url attachment, style_name def url attachment, style_name
raise InfiniteInterpolationError if attachment.options[:url].include?(":url") raise InfiniteInterpolationError if caller.any?{|b| b.index(RIGHT_HERE) }
attachment.url(style_name, false) attachment.url(style_name, false)
end end
# Returns the timestamp as defined by the <attachment>_updated_at field # Returns the timestamp as defined by the <attachment>_updated_at field
# in the server default time zone unless :use_global_time_zone is set
# to false. Note that a Rails.config.time_zone change will still
# invalidate any path or URL that uses :timestamp. For a
# time_zone-agnostic timestamp, use #updated_at.
def timestamp attachment, style_name def timestamp attachment, style_name
attachment.instance_read(:updated_at).to_s attachment.instance_read(:updated_at).in_time_zone(attachment.time_zone).to_s
end end
# Returns the RAILS_ROOT constant. # Returns an integer timestamp that is time zone-neutral, so that paths
# remain valid even if a server's time zone changes.
def updated_at attachment, style_name
attachment.updated_at
end
# Returns the Rails.root constant.
def rails_root attachment, style_name def rails_root attachment, style_name
RAILS_ROOT Rails.root
end end
# Returns the RAILS_ENV constant. # Returns the Rails.env constant.
def rails_env attachment, style_name def rails_env attachment, style_name
RAILS_ENV Rails.env
end end
# Returns the underscored, pluralized version of the class name. # Returns the underscored, pluralized version of the class name.
...@@ -83,15 +94,60 @@ module Paperclip ...@@ -83,15 +94,60 @@ 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.
def fingerprint attachment, style_name
attachment.fingerprint
end
# Returns a the attachment hash. See Paperclip::Attachment#hash for
# more details.
def hash attachment, style_name
attachment.hash(style_name)
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.
......
# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying # Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
# and Tempfile conversion. # and Tempfile conversion.
module IOStream module IOStream
# Returns a Tempfile containing the contents of the readable object. # Returns a Tempfile containing the contents of the readable object.
def to_tempfile def to_tempfile(object)
name = respond_to?(:original_filename) ? original_filename : (respond_to?(:path) ? path : "stream") return object.to_tempfile if object.respond_to?(:to_tempfile)
tempfile = Paperclip::Tempfile.new(File.basename(name)) name = object.respond_to?(:original_filename) ? object.original_filename : (object.respond_to?(:path) ? object.path : "stream")
tempfile = Paperclip::Tempfile.new(["stream", File.extname(name)])
tempfile.binmode tempfile.binmode
self.stream_to(tempfile) stream_to(object, tempfile)
end end
# Copies one read-able object from one place to another in blocks, obviating the need to load # Copies one read-able object from one place to another in blocks, obviating the need to load
# the whole thing into memory. Defaults to 8k blocks. If this module is included in both # the whole thing into memory. Defaults to 8k blocks. Returns a File if a String is passed
# StringIO and Tempfile, then either can have its data copied anywhere else without typing # in as the destination and returns the IO or Tempfile as passed in if one is sent as the destination.
# worries or memory overhead worries. Returns a File if a String is passed in as the destination def stream_to object, path_or_file, in_blocks_of = 8192
# and returns the IO or Tempfile as passed in if one is sent as the destination.
def stream_to path_or_file, in_blocks_of = 8192
dstio = case path_or_file dstio = case path_or_file
when String then File.new(path_or_file, "wb+") when String then File.new(path_or_file, "wb+")
when IO then path_or_file when IO then path_or_file
when Tempfile then path_or_file when Tempfile then path_or_file
end end
buffer = "" buffer = ""
self.rewind object.rewind
while self.read(in_blocks_of, buffer) do while object.read(in_blocks_of, buffer) do
dstio.write(buffer) dstio.write(buffer)
end end
dstio.rewind dstio.rewind
...@@ -31,20 +29,8 @@ module IOStream ...@@ -31,20 +29,8 @@ module IOStream
end end
end end
class IO #:nodoc:
include IOStream
end
%w( Tempfile StringIO ).each do |klass|
if Object.const_defined? klass
Object.const_get(klass).class_eval do
include IOStream
end
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
......
...@@ -2,3 +2,32 @@ require 'paperclip/matchers/have_attached_file_matcher' ...@@ -2,3 +2,32 @@ require 'paperclip/matchers/have_attached_file_matcher'
require 'paperclip/matchers/validate_attachment_presence_matcher' require 'paperclip/matchers/validate_attachment_presence_matcher'
require 'paperclip/matchers/validate_attachment_content_type_matcher' require 'paperclip/matchers/validate_attachment_content_type_matcher'
require 'paperclip/matchers/validate_attachment_size_matcher' require 'paperclip/matchers/validate_attachment_size_matcher'
module Paperclip
module Shoulda
# Provides rspec-compatible matchers for testing Paperclip attachments.
#
# In spec_helper.rb, you'll need to require the matchers:
#
# require "paperclip/matchers"
#
# And include the module:
#
# Spec::Runner.configure do |config|
# config.include Paperclip::Shoulda::Matchers
# end
#
# Example:
# describe User do
# it { should have_attached_file(:avatar) }
# it { should validate_attachment_presence(:avatar) }
# it { should validate_attachment_content_type(:avatar).
# allowing('image/png', 'image/gif').
# rejecting('text/plain', 'text/xml') }
# it { should validate_attachment_size(:avatar).
# less_than(2.megabytes) }
# end
module Matchers
end
end
end
module Paperclip module Paperclip
module Shoulda module Shoulda
module Matchers module Matchers
# Ensures that the given instance or class has an attachment with the
# given name.
#
# Example:
# describe User do
# it { should have_attached_file(:avatar) }
# end
def have_attached_file name def have_attached_file name
HaveAttachedFileMatcher.new(name) HaveAttachedFileMatcher.new(name)
end end
...@@ -12,6 +19,7 @@ module Paperclip ...@@ -12,6 +19,7 @@ module Paperclip
def matches? subject def matches? subject
@subject = subject @subject = subject
@subject = @subject.class unless Class === @subject
responds? && has_column? && included? responds? && has_column? && included?
end end
......
module Paperclip module Paperclip
module Shoulda module Shoulda
module Matchers module Matchers
# Ensures that the given instance or class validates the content type of
# the given attachment as specified.
#
# Example:
# describe User do
# it { should validate_attachment_content_type(:icon).
# allowing('image/png', 'image/gif').
# rejecting('text/plain', 'text/xml') }
# end
def validate_attachment_content_type name def validate_attachment_content_type name
ValidateAttachmentContentTypeMatcher.new(name) ValidateAttachmentContentTypeMatcher.new(name)
end end
...@@ -8,6 +17,8 @@ module Paperclip ...@@ -8,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
...@@ -22,18 +33,25 @@ module Paperclip ...@@ -22,18 +33,25 @@ module Paperclip
def matches? subject def matches? subject
@subject = subject @subject = subject
@subject = @subject.class unless Class === @subject
@allowed_types && @rejected_types && @allowed_types && @rejected_types &&
allowed_types_allowed? && rejected_types_rejected? allowed_types_allowed? && rejected_types_rejected?
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
...@@ -42,24 +60,22 @@ module Paperclip ...@@ -42,24 +60,22 @@ 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.errors.on(:"#{@attachment_name}_content_type").blank? subject.valid?
end subject.errors[:"#{@attachment_name}_content_type"].blank?
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
end end
end end
module Paperclip module Paperclip
module Shoulda module Shoulda
module Matchers module Matchers
# Ensures that the given instance or class validates the presence of the
# given attachment.
#
# describe User do
# it { should validate_attachment_presence(:avatar) }
# end
def validate_attachment_presence name def validate_attachment_presence name
ValidateAttachmentPresenceMatcher.new(name) ValidateAttachmentPresenceMatcher.new(name)
end end
...@@ -12,6 +18,7 @@ module Paperclip ...@@ -12,6 +18,7 @@ module Paperclip
def matches? subject def matches? subject
@subject = subject @subject = subject
@subject = @subject.class unless Class === @subject
error_when_not_valid? && no_error_when_valid? error_when_not_valid? && no_error_when_valid?
end end
...@@ -32,17 +39,16 @@ module Paperclip ...@@ -32,17 +39,16 @@ module Paperclip
def error_when_not_valid? def error_when_not_valid?
(subject = @subject.new).send(@attachment_name).assign(nil) (subject = @subject.new).send(@attachment_name).assign(nil)
subject.valid? subject.valid?
not subject.errors.on(:"#{@attachment_name}_file_name").blank? not subject.errors[:"#{@attachment_name}_file_name"].blank?
end end
def no_error_when_valid? def no_error_when_valid?
@file = StringIO.new(".") @file = StringIO.new(".")
(subject = @subject.new).send(@attachment_name).assign(@file) (subject = @subject.new).send(@attachment_name).assign(@file)
subject.valid? subject.valid?
subject.errors.on(:"#{@attachment_name}_file_name").blank? subject.errors[:"#{@attachment_name}_file_name"].blank?
end end
end end
end end
end end
end end
module Paperclip module Paperclip
module Shoulda module Shoulda
module Matchers module Matchers
# Ensures that the given instance or class validates the size of the
# given attachment as specified.
#
# Examples:
# it { should validate_attachment_size(:avatar).
# less_than(2.megabytes) }
# it { should validate_attachment_size(:icon).
# greater_than(1024) }
# it { should validate_attachment_size(:icon).
# in(0..100) }
def validate_attachment_size name def validate_attachment_size name
ValidateAttachmentSizeMatcher.new(name) ValidateAttachmentSizeMatcher.new(name)
end end
...@@ -28,6 +38,7 @@ module Paperclip ...@@ -28,6 +38,7 @@ module Paperclip
def matches? subject def matches? subject
@subject = subject @subject = subject
@subject = @subject.class unless Class === @subject
lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high? lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
end end
...@@ -58,7 +69,7 @@ module Paperclip ...@@ -58,7 +69,7 @@ module Paperclip
(subject = @subject.new).send(@attachment_name).assign(file) (subject = @subject.new).send(@attachment_name).assign(file)
subject.valid? subject.valid?
subject.errors.on(:"#{@attachment_name}_file_size").blank? subject.errors[:"#{@attachment_name}_file_size"].blank?
end end
def lower_than_low? def lower_than_low?
...@@ -82,4 +93,3 @@ module Paperclip ...@@ -82,4 +93,3 @@ module Paperclip
end end
end end
end end
...@@ -40,10 +40,19 @@ module Paperclip ...@@ -40,10 +40,19 @@ module Paperclip
# on this blog post: # on this blog post:
# 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
# Replaces Tempfile's +make_tmpname+ with one that honors file extensions. # This is Ruby 1.8.7's implementation.
if RUBY_VERSION <= "1.8.6" || RUBY_PLATFORM =~ /java/
def make_tmpname(basename, n) def make_tmpname(basename, n)
extension = File.extname(basename) case basename
sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension) when Array
prefix, suffix = *basename
else
prefix, suffix = basename, ''
end
t = Time.now.strftime("%y%m%d")
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}-#{n}#{suffix}"
end
end end
end end
end end
require 'paperclip'
module Paperclip
if defined? Rails::Railtie
require 'rails'
class Railtie < Rails::Railtie
initializer 'paperclip.insert_into_active_record' do
ActiveSupport.on_load :active_record do
Paperclip::Railtie.insert
end
end
rake_tasks do
load "tasks/paperclip.rake"
end
end
end
class Railtie
def self.insert
ActiveRecord::Base.send(:include, Paperclip::Glue)
File.send(:include, Paperclip::Upfile)
end
end
end
module Paperclip require "paperclip/storage/filesystem"
module Storage require "paperclip/storage/fog"
require "paperclip/storage/s3"
# The default place to store attachments is in the filesystem. Files on the local
# filesystem can be very easily served by Apache without requiring a hit to your app.
# They also can be processed more easily after they've been saved, as they're just
# normal files. There is one Filesystem-specific option for has_attached_file.
# * +path+: The location of the repository of attachments on disk. This can (and, in
# almost all cases, should) be coordinated with the value of the +url+ option to
# allow files to be saved into a place where Apache can serve them without
# hitting your app. Defaults to
# ":rails_root/public/:attachment/:id/:style/:basename.:extension"
# By default this places the files in the app's public directory which can be served
# directly. If you are using capistrano for deployment, a good idea would be to
# make a symlink to the capistrano-created system directory from inside your app's
# public directory.
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
module Filesystem
def self.extended base
end
def exists?(style_name = default_style)
if original_filename
File.exist?(path(style_name))
else
false
end
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_name = default_style
@queued_for_write[style_name] || (File.new(path(style_name), 'rb') if exists?(style_name))
end
def flush_writes #:nodoc:
@queued_for_write.each do |style_name, file|
file.close
FileUtils.mkdir_p(File.dirname(path(style_name)))
log("saving #{path(style_name)}")
FileUtils.mv(file.path, path(style_name))
FileUtils.chmod(0644, path(style_name))
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
log("deleting #{path}")
FileUtils.rm(path) if File.exist?(path)
rescue Errno::ENOENT => e
# ignore file-not-found, let everything else pass
end
begin
while(true)
path = File.dirname(path)
FileUtils.rmdir(path)
end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
# Stop trying to remove parent directories
rescue SystemCallError => e
log("There was an unexpected error while deleting directories: #{e.class}")
# Ignore it
end
end
@queued_for_delete = []
end
end
# 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 S3-specific options for has_attached_file:
# * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
# gives you. You can 'environment-space' this just like you do to your
# database.yml file, so different environments can use different accounts:
# development:
# access_key_id: 123...
# secret_access_key: 123...
# test:
# access_key_id: abc...
# secret_access_key: abc...
# production:
# access_key_id: 456...
# secret_access_key: 456...
# This is not required, however, and the file may simply look like this:
# access_key_id: 456...
# secret_access_key: 456...
# In which case, those access keys will be used in all environments. You can also
# put your bucket name in this file, instead of adding it to the code directly.
# This is useful when you want the same account but a different bucket for
# development versus production.
# * +s3_permissions+: This is a String that should be one of the "canned" access
# policies that S3 provides (more information can be found here:
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
# The default for Paperclip is :public_read.
# * +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
# default), and 'https' when your :s3_permissions are anything else.
# * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
# * +bucket+: 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. The bucket name will not be interpolated.
# You can define the bucket as a Proc if you want to determine it's name at runtime.
# Paperclip will call that Proc with attachment as the only argument.
# * +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
# 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
# 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
# :s3_alias_url. You can read more about CNAMEs and S3 at
# 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
# path-style, or :s3_path_url). But in some cases paths don't work and you need to use
# the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
# 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
# alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
# by S3.
# * +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.
module S3
def self.extended base
begin
require 'aws/s3'
rescue LoadError => e
e.message << " (You may need to install the aws-s3 gem)"
raise e
end
base.instance_eval do
@s3_credentials = parse_credentials(@options[:s3_credentials])
@bucket = @options[:bucket] || @s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
@s3_options = @options[:s3_options] || {}
@s3_permissions = @options[:s3_permissions] || :public_read
@s3_protocol = @options[:s3_protocol] || (@s3_permissions == :public_read ? 'http' : 'https')
@s3_headers = @options[:s3_headers] || {}
@s3_host_alias = @options[:s3_host_alias]
@url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
AWS::S3::Base.establish_connection!( @s3_options.merge(
:access_key_id => @s3_credentials[:access_key_id],
:secret_access_key => @s3_credentials[:secret_access_key]
))
end
Paperclip.interpolates(:s3_alias_url) do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
end
Paperclip.interpolates(:s3_path_url) do |attachment, style|
"#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
end
Paperclip.interpolates(:s3_domain_url) do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
end
end
def expiring_url(time = 3600)
AWS::S3::S3Object.url_for(path, bucket_name, :expires_in => time )
end
def bucket_name
@bucket
end
def s3_host_alias
@s3_host_alias
end
def parse_credentials creds
creds = find_credentials(creds).stringify_keys
(creds[RAILS_ENV] || creds).symbolize_keys
end
def exists?(style = default_style)
if original_filename
AWS::S3::S3Object.exists?(path(style), bucket_name)
else
false
end
end
def s3_protocol
@s3_protocol
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
return @queued_for_write[style] if @queued_for_write[style]
file = Tempfile.new(path(style))
file.write(AWS::S3::S3Object.value(path(style), bucket_name))
file.rewind
return file
end
def flush_writes #:nodoc:
@queued_for_write.each do |style, file|
begin
log("saving #{path(style)}")
AWS::S3::S3Object.store(path(style),
file,
bucket_name,
{:content_type => instance_read(:content_type),
:access => @s3_permissions,
}.merge(@s3_headers))
rescue AWS::S3::ResponseError => e
raise
end
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
log("deleting #{path}")
AWS::S3::S3Object.delete(path, bucket_name)
rescue AWS::S3::ResponseError
# Ignore this.
end
end
@queued_for_delete = []
end
def find_credentials creds
case creds
when File
YAML::load(ERB.new(File.read(creds.path)).result)
when String
YAML::load(ERB.new(File.read(creds)).result)
when Hash
creds
else
raise ArgumentError, "Credentials are not a path, file, or hash."
end
end
private :find_credentials
end
end
end
module Paperclip
module Storage
# The default place to store attachments is in the filesystem. Files on the local
# filesystem can be very easily served by Apache without requiring a hit to your app.
# They also can be processed more easily after they've been saved, as they're just
# normal files. There is one Filesystem-specific option for has_attached_file.
# * +path+: The location of the repository of attachments on disk. This can (and, in
# almost all cases, should) be coordinated with the value of the +url+ option to
# allow files to be saved into a place where Apache can serve them without
# hitting your app. Defaults to
# ":rails_root/public/:attachment/:id/:style/:basename.:extension"
# By default this places the files in the app's public directory which can be served
# directly. If you are using capistrano for deployment, a good idea would be to
# make a symlink to the capistrano-created system directory from inside your app's
# public directory.
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
module Filesystem
def self.extended base
end
def exists?(style_name = default_style)
if original_filename
File.exist?(path(style_name))
else
false
end
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_name = default_style
@queued_for_write[style_name] || (File.new(path(style_name), 'rb') if exists?(style_name))
end
def flush_writes #:nodoc:
@queued_for_write.each do |style_name, file|
file.close
FileUtils.mkdir_p(File.dirname(path(style_name)))
log("saving #{path(style_name)}")
begin
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
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
log("deleting #{path}")
FileUtils.rm(path) if File.exist?(path)
rescue Errno::ENOENT => e
# ignore file-not-found, let everything else pass
end
begin
while(true)
path = File.dirname(path)
FileUtils.rmdir(path)
break if File.exists?(path) # Ruby 1.9.2 does not raise if the removal failed.
end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
# Stop trying to remove parent directories
rescue SystemCallError => e
log("There was an unexpected error while deleting directories: #{e.class}")
# Ignore it
end
end
@queued_for_delete = []
end
end
end
end
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
@fog_directory = @options[:fog_directory]
@fog_credentials = @options[:fog_credentials]
@fog_host = @options[:fog_host]
@fog_public = @options.key?(:fog_public) ? @options[:fog_public] : true
@fog_file = @options[:fog_file] || {}
@url = ':fog_public_url'
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 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
@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 @fog_host
host = (@fog_host =~ /%d/) ? @fog_host % (path(style).hash % 4) : @fog_host
"#{host}/#{path(style)}"
else
directory.files.new(:key => path(style)).public_url
end
end
private
def connection
@connection ||= ::Fog::Storage.new(@fog_credentials)
end
def directory
@directory ||= connection.directories.new(:key => @fog_directory)
end
end
end
end
module Paperclip
module Storage
# 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 S3-specific options for has_attached_file:
# * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
# gives you. You can 'environment-space' this just like you do to your
# database.yml file, so different environments can use different accounts:
# development:
# access_key_id: 123...
# secret_access_key: 123...
# test:
# access_key_id: abc...
# secret_access_key: abc...
# production:
# access_key_id: 456...
# secret_access_key: 456...
# This is not required, however, and the file may simply look like this:
# access_key_id: 456...
# secret_access_key: 456...
# In which case, those access keys will be used in all environments. You can also
# put your bucket name in this file, instead of adding it to the code directly.
# This is useful when you want the same account but a different bucket for
# development versus production.
# * +s3_permissions+: This is a String that should be one of the "canned" access
# policies that S3 provides (more information can be found here:
# http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAccessPolicy.html)
# 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
# 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
# default), and 'https' when your :s3_permissions are anything else.
# * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
# * +bucket+: 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. The bucket name will not be interpolated.
# You can define the bucket as a Proc if you want to determine it's name at runtime.
# Paperclip will call that Proc with attachment as the only argument.
# * +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
# link in the +url+ entry for more information about S3 domains and buckets.
# * +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).
# 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
# 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
# path-style, or :s3_path_url). But in some cases paths don't work and you need to use
# the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
# 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
# alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
# 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
# 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.
# * +s3_host_name+: If you are using your bucket in Tokyo region etc, write host_name.
module S3
def self.extended base
begin
require 'aws/s3'
rescue LoadError => e
e.message << " (You may need to install the aws-s3 gem)"
raise e
end unless defined?(AWS::S3)
base.instance_eval do
@s3_credentials = parse_credentials(@options[:s3_credentials])
@s3_host_name = @options[:s3_host_name] || @s3_credentials[:s3_host_name]
@bucket = @options[:bucket] || @s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
@s3_options = @options[:s3_options] || {}
@s3_permissions = set_permissions(@options[:s3_permissions])
@s3_protocol = @options[:s3_protocol] ||
Proc.new do |style|
(@s3_permissions[style.to_sym] || @s3_permissions[:default]) == :public_read ? 'http' : 'https'
end
@s3_headers = @options[:s3_headers] || {}
@s3_host_alias = @options[:s3_host_alias]
@s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.is_a?(Proc)
unless @url.to_s.match(/^:s3.*url$/)
@path = @path.gsub(/:url/, @url)
@url = ":s3_path_url"
end
@url = ":asset_host" if @options[:url].to_s == ":asset_host"
AWS::S3::Base.establish_connection!( @s3_options.merge(
:access_key_id => @s3_credentials[:access_key_id],
:secret_access_key => @s3_credentials[:secret_access_key]
))
end
Paperclip.interpolates(:s3_alias_url) do |attachment, style|
"#{attachment.s3_protocol(style)}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
end unless Paperclip::Interpolations.respond_to? :s3_alias_url
Paperclip.interpolates(:s3_path_url) do |attachment, style|
"#{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
Paperclip.interpolates(:s3_domain_url) do |attachment, style|
"#{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
Paperclip.interpolates(:asset_host) do |attachment, style|
"#{attachment.path(style).gsub(%r{^/}, "")}"
end unless Paperclip::Interpolations.respond_to? :asset_host
end
def expiring_url(time = 3600, style_name = default_style)
AWS::S3::S3Object.url_for(path(style_name), bucket_name, :expires_in => time, :use_ssl => (s3_protocol(style_name) == 'https'))
end
def bucket_name
@bucket
end
def s3_host_name
@s3_host_name || "s3.amazonaws.com"
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 s3_host_alias
@s3_host_alias
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
def exists?(style = default_style)
if original_filename
AWS::S3::S3Object.exists?(path(style), bucket_name)
else
false
end
end
def s3_protocol(style)
if @s3_protocol.is_a?(Proc)
@s3_protocol.call(style)
else
@s3_protocol
end
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
return @queued_for_write[style] if @queued_for_write[style]
filename = path(style)
extname = File.extname(filename)
basename = File.basename(filename, extname)
file = Tempfile.new([basename, extname])
file.binmode
file.write(AWS::S3::S3Object.value(path(style), bucket_name))
file.rewind
return file
end
def create_bucket
AWS::S3::Bucket.create(bucket_name)
end
def flush_writes #:nodoc:
@queued_for_write.each do |style, file|
begin
log("saving #{path(style)}")
AWS::S3::S3Object.store(path(style),
file,
bucket_name,
{:content_type => file.content_type.to_s.strip,
:access => (@s3_permissions[style] || @s3_permissions[:default]),
}.merge(@s3_headers))
rescue AWS::S3::NoSuchBucket => e
create_bucket
retry
rescue AWS::S3::ResponseError => e
raise
end
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
log("deleting #{path}")
AWS::S3::S3Object.delete(path, bucket_name)
rescue AWS::S3::ResponseError
# Ignore this.
end
end
@queued_for_delete = []
end
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
private :find_credentials
end
end
end
...@@ -30,8 +30,9 @@ module Paperclip ...@@ -30,8 +30,9 @@ 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.processors)
end end
# retrieves from the attachment the whiny setting # retrieves from the attachment the whiny setting
...@@ -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,7 +67,7 @@ module Paperclip ...@@ -62,7 +67,7 @@ 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
...@@ -71,7 +76,7 @@ module Paperclip ...@@ -71,7 +76,7 @@ module Paperclip
# 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.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+
...@@ -22,6 +26,10 @@ module Paperclip ...@@ -22,6 +26,10 @@ module Paperclip
@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)
@convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
@current_format = File.extname(@file.path) @current_format = File.extname(@file.path)
@basename = File.basename(@file.path, @current_format) @basename = File.basename(@file.path, @current_format)
...@@ -42,20 +50,24 @@ module Paperclip ...@@ -42,20 +50,24 @@ module Paperclip
# that contains the new image. # that contains the new image.
def make def make
src = @file src = @file
dst = Tempfile.new([@basename, @format].compact.join(".")) dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
dst.binmode dst.binmode
command = <<-end_command
#{ source_file_options }
"#{ File.expand_path(src.path) }[0]"
#{ transformation_command }
"#{ File.expand_path(dst.path) }"
end_command
begin begin
success = Paperclip.run("convert", command.gsub(/\s+/, " ")) parameters = []
rescue PaperclipCommandLineError parameters << source_file_options
parameters << ":source"
parameters << transformation_command
parameters << convert_options
parameters << ":dest"
parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}#{'[0]' unless animated?}", :dest => File.expand_path(dst.path))
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
...@@ -65,11 +77,18 @@ module Paperclip ...@@ -65,11 +77,18 @@ module Paperclip
# into the thumbnail. # into the thumbnail.
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 << " -resize \"#{scale}\"" unless scale.nil? || scale.empty? trans << "-coalesce" if animated?
trans << " -crop \"#{crop}\" +repage" if crop trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
trans << " #{convert_options}" if convert_options? 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,18 +8,26 @@ module Paperclip ...@@ -6,18 +8,26 @@ 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
Paperclip.run("file", "--mime-type #{self.path}").split(':').last.strip rescue "application/x-#{type}" iterate_over_array_to_find_best_option(types)
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-type :file", :file => self.path).split(':').last.strip rescue "application/x-#{type}")
mime_type = "application/x-#{type}" if mime_type.match(/\(.*?\)/)
mime_type
end end
# Returns the file's normal name. # Returns the file's normal name.
...@@ -34,17 +44,19 @@ end ...@@ -34,17 +44,19 @@ end
if defined? StringIO if defined? StringIO
class StringIO class StringIO
attr_accessor :original_filename, :content_type attr_accessor :original_filename, :content_type, :fingerprint
def original_filename def original_filename
@original_filename ||= "stringio.txt" @original_filename ||= "stringio.txt"
end end
def content_type def content_type
@content_type ||= "text/plain" @content_type ||= "text/plain"
end end
def fingerprint
@fingerprint ||= Digest::MD5.hexdigest(self.string)
end
end end
end end
class File #:nodoc: class File #:nodoc:
include Paperclip::Upfile include Paperclip::Upfile
end end
module Paperclip
VERSION = "2.3.16" unless defined? Paperclip::VERSION
end
def obtain_class module Paperclip
module Task
def self.obtain_class
class_name = ENV['CLASS'] || ENV['class'] class_name = ENV['CLASS'] || ENV['class']
raise "Must specify CLASS" unless class_name raise "Must specify CLASS" unless class_name
@klass = Object.const_get(class_name) class_name
end end
def obtain_attachments def self.obtain_attachments(klass)
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
def for_all_attachments
klass = obtain_class
names = obtain_attachments
ids = klass.connection.select_values(klass.send(:construct_finder_sql, :select => 'id'))
ids.each do |id|
instance = klass.find(id)
names.each do |name|
result = if instance.send("#{ name }?")
yield(instance, name)
else
true
end end
print result ? "." : "x"; $stdout.flush
end end
end end
puts " Done."
end end
namespace :paperclip do namespace :paperclip do
...@@ -38,35 +24,50 @@ namespace :paperclip do ...@@ -38,35 +24,50 @@ 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 = []
for_all_attachments do |instance, name| klass = Paperclip::Task.obtain_class
result = instance.send(name).reprocess! names = Paperclip::Task.obtain_attachments(klass)
styles = (ENV['STYLES'] || ENV['styles'] || '').split(',').map(&:to_sym)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
instance.send(name).reprocess!(*styles)
errors << [instance.id, instance.errors] unless instance.errors.blank? errors << [instance.id, instance.errors] unless instance.errors.blank?
result end
end end
errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" } errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
end end
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
for_all_attachments do |instance, name| klass = Paperclip::Task.obtain_class
if file = instance.send(name).to_file names = Paperclip::Task.obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
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")
if Rails.version >= "3.0.0"
instance.save(:validate => false)
else
instance.save(false) instance.save(false)
end
else else
true true
end end
end end
end end
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
for_all_attachments do |instance, name| klass = Paperclip::Task.obtain_class
names = Paperclip::Task.obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
instance.send(name).send(:validate) instance.send(name).send(:validate)
if instance.send(name).valid? if instance.send(name).valid?
true true
...@@ -76,4 +77,5 @@ namespace :paperclip do ...@@ -76,4 +77,5 @@ namespace :paperclip do
end end
end end
end end
end
end end
# -*- encoding: utf-8 -*- $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
require 'paperclip/version'
Gem::Specification.new do |s| include_files = ["README*", "LICENSE", "Rakefile", "init.rb", "{lib,tasks,test,rails,generators,shoulda_macros}/**/*"].map do |glob|
s.name = %q{monde-paperclip} Dir[glob]
s.version = "2.3.1.1" end.flatten
exclude_files = ["**/*.rbc", "test/s3.yml", "test/debug.log", "test/paperclip.db", "test/doc", "test/doc/*", "test/pkg", "test/pkg/*", "test/tmp", "test/tmp/*"].map do |glob|
Dir[glob]
end.flatten
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= spec = Gem::Specification.new do |s|
s.authors = ["Jon Yurek"] s.name = "paperclip"
s.date = %q{2009-10-09} s.version = Paperclip::VERSION
s.email = %q{jyurek@thoughtbot.com} s.author = "Jon Yurek"
s.extra_rdoc_files = ["README.rdoc"] s.email = "jyurek@thoughtbot.com"
s.files = ["README.rdoc", "LICENSE", "Rakefile", "init.rb", "generators/paperclip", "generators/paperclip/paperclip_generator.rb", "generators/paperclip/templates", "generators/paperclip/templates/paperclip_migration.rb.erb", "generators/paperclip/USAGE", "lib/paperclip", "lib/paperclip/attachment.rb", "lib/paperclip/callback_compatability.rb", "lib/paperclip/geometry.rb", "lib/paperclip/interpolations.rb", "lib/paperclip/iostream.rb", "lib/paperclip/matchers", "lib/paperclip/matchers/have_attached_file_matcher.rb", "lib/paperclip/matchers/validate_attachment_content_type_matcher.rb", "lib/paperclip/matchers/validate_attachment_presence_matcher.rb", "lib/paperclip/matchers/validate_attachment_size_matcher.rb", "lib/paperclip/matchers.rb", "lib/paperclip/processor.rb", "lib/paperclip/storage.rb", "lib/paperclip/thumbnail.rb", "lib/paperclip/upfile.rb", "lib/paperclip.rb", "tasks/paperclip_tasks.rake", "test/attachment_test.rb", "test/database.yml", "test/fixtures", "test/fixtures/12k.png", "test/fixtures/50x50.png", "test/fixtures/5k.png", "test/fixtures/bad.png", "test/fixtures/s3.yml", "test/fixtures/text.txt", "test/fixtures/twopage.pdf", "test/geometry_test.rb", "test/helper.rb", "test/integration_test.rb", "test/interpolations_test.rb", "test/iostream_test.rb", "test/matchers", "test/matchers/have_attached_file_matcher_test.rb", "test/matchers/validate_attachment_content_type_matcher_test.rb", "test/matchers/validate_attachment_presence_matcher_test.rb", "test/matchers/validate_attachment_size_matcher_test.rb", "test/paperclip_test.rb", "test/processor_test.rb", "test/storage_test.rb", "test/thumbnail_test.rb", "test/upfile_test.rb", "shoulda_macros/paperclip.rb"] s.homepage = "https://github.com/thoughtbot/paperclip"
s.homepage = %q{http://www.thoughtbot.com/projects/paperclip} s.description = "Easy upload management for ActiveRecord"
s.rdoc_options = ["--line-numbers", "--inline-source"] s.platform = Gem::Platform::RUBY
s.require_paths = ["lib"] s.summary = "File attachments as attributes for ActiveRecord"
s.requirements = ["ImageMagick"] s.files = include_files - exclude_files
s.rubyforge_project = %q{paperclip} s.require_path = "lib"
s.rubygems_version = %q{1.3.5} s.test_files = Dir["test/**/test_*.rb"]
s.summary = %q{File attachments as attributes for ActiveRecord} s.rubyforge_project = "paperclip"
s.extra_rdoc_files = Dir["README*"]
if s.respond_to? :specification_version then s.rdoc_options << '--line-numbers' << '--inline-source'
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION s.requirements << "ImageMagick"
s.specification_version = 3 s.add_dependency 'activerecord', '>=2.3.0'
s.add_dependency 'activesupport', '>=2.3.2'
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then s.add_dependency 'cocaine', '>=0.0.2'
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"]) s.add_dependency 'mime-types'
s.add_development_dependency(%q<jferris-mocha>, ["= 0.9.5.0.1241126838"]) s.add_development_dependency 'shoulda'
s.add_development_dependency(%q<aws-s3>, [">= 0"]) s.add_development_dependency 'appraisal'
s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"]) s.add_development_dependency 'mocha'
s.add_development_dependency(%q<activerecord>, [">= 0"]) s.add_development_dependency 'aws-s3'
s.add_development_dependency(%q<activesupport>, [">= 0"]) s.add_development_dependency 'sqlite3'
else
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
s.add_dependency(%q<jferris-mocha>, ["= 0.9.5.0.1241126838"])
s.add_dependency(%q<aws-s3>, [">= 0"])
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
s.add_dependency(%q<activerecord>, [">= 0"])
s.add_dependency(%q<activesupport>, [">= 0"])
end
else
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
s.add_dependency(%q<jferris-mocha>, ["= 0.9.5.0.1241126838"])
s.add_dependency(%q<aws-s3>, [">= 0"])
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
s.add_dependency(%q<activerecord>, [">= 0"])
s.add_dependency(%q<activesupport>, [">= 0"])
end
end end
require 'paperclip/railtie'
Paperclip::Railtie.insert
require 'paperclip/matchers' require 'paperclip/matchers'
require 'action_controller'
module Paperclip module Paperclip
# =Paperclip Shoulda Macros # =Paperclip Shoulda Macros
...@@ -104,8 +103,10 @@ module Paperclip ...@@ -104,8 +103,10 @@ module Paperclip
end end
end end
class ActionController::Integration::Session #:nodoc: if defined?(ActionController::Integration::Session)
class ActionController::Integration::Session #:nodoc:
include Paperclip::Shoulda include Paperclip::Shoulda
end
end end
class Factory class Factory
......
# encoding: utf-8 # encoding: utf-8
require 'test/helper' require './test/helper'
class Dummy class Dummy
# This is a dummy class # This is a dummy class
...@@ -11,7 +11,29 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -11,7 +11,29 @@ class AttachmentTest < Test::Unit::TestCase
@model = @attachment.instance @model = @attachment.instance
@model.id = 1234 @model.id = 1234
@model.avatar_file_name = "fake.jpg" @model.avatar_file_name = "fake.jpg"
assert_equal "#{RAILS_ROOT}/public/fake_models/1234/fake", @attachment.path assert_equal "#{Rails.root}/public/fake_models/1234/fake", @attachment.path
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 end
context "Attachment default_options" do context "Attachment default_options" do
...@@ -76,6 +98,19 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -76,6 +98,19 @@ class AttachmentTest < Test::Unit::TestCase
end end
end end
end end
context "with nested hash default" do
setup do
@nested_hash = {:thumb => {:first => "second" }}
Paperclip::Attachment.default_options[:styles] = @nested_hash
@dummy = Dummy.new
@attachment = @dummy.avatar
end
should "correctly clone the nested hash" do
assert_equal(@nested_hash, @attachment.instance_variable_get(:@styles))
end
end
end end
end end
...@@ -97,6 +132,83 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -97,6 +132,83 @@ class AttachmentTest < Test::Unit::TestCase
end end
end end
context "An attachment with :timestamp interpolations" do
setup do
@file = StringIO.new("...")
@zone = 'UTC'
Time.stubs(:zone).returns(@zone)
@zone_default = 'Eastern Time (US & Canada)'
Time.stubs(:zone_default).returns(@zone_default)
end
context "using default time zone" do
setup do
rebuild_model :path => ":timestamp", :use_default_time_zone => true
@dummy = Dummy.new
@dummy.avatar = @file
end
should "return a time in the default zone" do
assert_equal @dummy.avatar_updated_at.in_time_zone(@zone_default).to_s, @dummy.avatar.path
end
end
context "using per-thread time zone" do
setup do
rebuild_model :path => ":timestamp", :use_default_time_zone => false
@dummy = Dummy.new
@dummy.avatar = @file
end
should "return a time in the per-thread zone" do
assert_equal @dummy.avatar_updated_at.in_time_zone(@zone).to_s, @dummy.avatar.path
end
end
end
context "An attachment with :hash interpolations" do
setup do
@file = StringIO.new("...")
end
should "raise if no secret is provided" do
@attachment = attachment :path => ":hash"
@attachment.assign @file
assert_raise ArgumentError do
@attachment.path
end
end
context "when secret is set" do
setup do
@attachment = attachment :path => ":hash", :hash_secret => "w00t"
@attachment.stubs(:instance_read).with(:updated_at).returns(Time.at(1234567890))
@attachment.stubs(:instance_read).with(:file_name).returns("bla.txt")
@attachment.instance.id = 1234
@attachment.assign @file
end
should "interpolate the hash data" do
@attachment.expects(:interpolate).with(@attachment.options[:hash_data],anything).returns("interpolated_stuff")
@attachment.hash
end
should "result in the correct interpolation" do
assert_equal "fake_models/avatars/1234/original/1234567890", @attachment.send(:interpolate,@attachment.options[:hash_data])
end
should "result in a correct hash" do
assert_equal "d22b617d1bf10016aa7d046d16427ae203f39fce", @attachment.path
end
should "generate a hash digest with the correct style" do
OpenSSL::HMAC.expects(:hexdigest).with(anything, anything, "fake_models/avatars/1234/medium/1234567890")
@attachment.path("medium")
end
end
end
context "An attachment with a :rails_env interpolation" do context "An attachment with a :rails_env interpolation" do
setup do setup do
@rails_env = "blah" @rails_env = "blah"
...@@ -106,12 +218,11 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -106,12 +218,11 @@ class AttachmentTest < Test::Unit::TestCase
@dummy.stubs(:id).returns(@id) @dummy.stubs(:id).returns(@id)
@file = StringIO.new(".") @file = StringIO.new(".")
@dummy.avatar = @file @dummy.avatar = @file
Rails.stubs(:env).returns(@rails_env)
end end
should "return the proper path" do should "return the proper path" do
temporary_rails_env(@rails_env) {
assert_equal "#{@rails_env}/#{@id}.png", @dummy.avatar.path assert_equal "#{@rails_env}/#{@id}.png", @dummy.avatar.path
}
end end
end end
...@@ -152,6 +263,47 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -152,6 +263,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.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 => {
...@@ -212,6 +364,24 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -212,6 +364,24 @@ class AttachmentTest < Test::Unit::TestCase
end end
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.styles[:thumb][:geometry]
assert_nil @dummy.avatar.styles[:large]
@dummy.other = 'b'
assert_equal "400x400", @dummy.avatar.styles[:large][:geometry]
assert_nil @dummy.avatar.styles[:thumb]
end
end
context "An attachment with :url that is a proc" do context "An attachment with :url that is a proc" do
setup do setup do
rebuild_model :url => lambda{ |attachment| "path/#{attachment.instance.other}.:extension" } rebuild_model :url => lambda{ |attachment| "path/#{attachment.instance.other}.:extension" }
...@@ -329,7 +499,12 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -329,7 +499,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
...@@ -339,9 +514,42 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -339,9 +514,42 @@ class AttachmentTest < Test::Unit::TestCase
end end
end end
should "include the filesystem module when loading the filesystem storage" do
rebuild_model :storage => :filesystem
@dummy = Dummy.new
assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem)
end
should "include the filesystem module even if capitalization is wrong" do
rebuild_model :storage => :FileSystem
@dummy = Dummy.new
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
should "raise an error if you try to include a storage module that doesn't exist" do
rebuild_model :storage => :not_here
@dummy = Dummy.new
assert_raises(Paperclip::StorageMethodNotFound) do
@dummy.avatar
end
end
context "An attachment with styles but no processors defined" do context "An attachment with styles but no processors defined" do
setup do setup do
rebuild_model :processors => [], :styles => {:something => 1} rebuild_model :processors => [], :styles => {:something => '1'}
@dummy = Dummy.new @dummy = Dummy.new
@file = StringIO.new("...") @file = StringIO.new("...")
end end
...@@ -363,7 +571,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -363,7 +571,7 @@ class AttachmentTest < Test::Unit::TestCase
context "Assigning an attachment with post_process hooks" do context "Assigning an attachment with post_process hooks" do
setup do setup do
rebuild_model :styles => { :something => "100x100#" } rebuild_class :styles => { :something => "100x100#" }
Dummy.class_eval do Dummy.class_eval do
before_avatar_post_process :do_before_avatar before_avatar_post_process :do_before_avatar
after_avatar_post_process :do_after_avatar after_avatar_post_process :do_after_avatar
...@@ -403,16 +611,16 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -403,16 +611,16 @@ class AttachmentTest < Test::Unit::TestCase
@dummy.expects(:do_before_avatar).never @dummy.expects(:do_before_avatar).never
@dummy.expects(:do_after_avatar).never @dummy.expects(:do_after_avatar).never
@dummy.expects(:do_before_all).with().returns(false) @dummy.expects(:do_before_all).with().returns(false)
@dummy.expects(:do_after_all).never @dummy.expects(:do_after_all)
Paperclip::Thumbnail.expects(:make).never Paperclip::Thumbnail.expects(:make).never
@dummy.avatar = @file @dummy.avatar = @file
end end
should "cancel the processing if a before_avatar_post_process returns false" do should "cancel the processing if a before_avatar_post_process returns false" do
@dummy.expects(:do_before_avatar).with().returns(false) @dummy.expects(:do_before_avatar).with().returns(false)
@dummy.expects(:do_after_avatar).never @dummy.expects(:do_after_avatar)
@dummy.expects(:do_before_all).with().returns(true) @dummy.expects(:do_before_all).with().returns(true)
@dummy.expects(:do_after_all).never @dummy.expects(:do_after_all)
Paperclip::Thumbnail.expects(:make).never Paperclip::Thumbnail.expects(:make).never
@dummy.avatar = @file @dummy.avatar = @file
end end
...@@ -422,15 +630,11 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -422,15 +630,11 @@ class AttachmentTest < Test::Unit::TestCase
setup do setup do
rebuild_model :styles => { :something => "100x100#" } rebuild_model :styles => { :something => "100x100#" }
@file = StringIO.new(".") @file = StringIO.new(".")
@file.expects(:original_filename).returns("5k.png\n\n") @file.stubs(:original_filename).returns("5k.png\n\n")
@file.expects(:content_type).returns("image/png\n\n") @file.stubs(:content_type).returns("image/png\n\n")
@file.stubs(:to_tempfile).returns(@file) @file.stubs(:to_tempfile).returns(@file)
@dummy = Dummy.new @dummy = Dummy.new
Paperclip::Thumbnail.expects(:make).returns(@file) Paperclip::Thumbnail.expects(:make).returns(@file)
@dummy.expects(:run_callbacks).with(:before_avatar_post_process, {:original => @file})
@dummy.expects(:run_callbacks).with(:before_post_process, {:original => @file})
@dummy.expects(:run_callbacks).with(:after_avatar_post_process, {:original => @file, :something => @file})
@dummy.expects(:run_callbacks).with(:after_post_process, {:original => @file, :something => @file})
@attachment = @dummy.avatar @attachment = @dummy.avatar
@dummy.avatar = @file @dummy.avatar = @file
end end
...@@ -448,12 +652,11 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -448,12 +652,11 @@ class AttachmentTest < Test::Unit::TestCase
setup do setup do
rebuild_model rebuild_model
@not_file = mock @not_file = mock("not_file")
@tempfile = mock @tempfile = mock("tempfile")
@not_file.stubs(:nil?).returns(false) @not_file.stubs(:nil?).returns(false)
@not_file.expects(:size).returns(10) @not_file.expects(:size).returns(10)
@tempfile.expects(:size).returns(10) @tempfile.expects(:size).returns(10)
@not_file.expects(:to_tempfile).returns(@tempfile)
@not_file.expects(:original_filename).returns("sheep_say_bæ.png\r\n") @not_file.expects(:original_filename).returns("sheep_say_bæ.png\r\n")
@not_file.expects(:content_type).returns("image/png\r\n") @not_file.expects(:content_type).returns("image/png\r\n")
...@@ -462,13 +665,55 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -462,13 +665,55 @@ class AttachmentTest < Test::Unit::TestCase
@attachment.expects(:valid_assignment?).with(@not_file).returns(true) @attachment.expects(:valid_assignment?).with(@not_file).returns(true)
@attachment.expects(:queue_existing_for_delete) @attachment.expects(:queue_existing_for_delete)
@attachment.expects(:post_process) @attachment.expects(:post_process)
@attachment.expects(:to_tempfile).returns(@tempfile)
@attachment.expects(:generate_fingerprint).with(@tempfile).returns("12345")
@attachment.expects(:generate_fingerprint).with(@not_file).returns("12345")
@dummy.avatar = @not_file @dummy.avatar = @not_file
end end
should "remove strange letters and replace with underscore (_)" do should "not remove strange letters" do
assert_equal "sheep_say_b_.png", @dummy.avatar.original_filename assert_equal "sheep_say_bæ.png", @dummy.avatar.original_filename
end
end end
context "Attachment with uppercase extension and a default style" do
setup do
@old_defaults = Paperclip::Attachment.default_options.dup
Paperclip::Attachment.default_options.merge!({
:path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
})
FileUtils.rm_rf("tmp")
rebuild_model
@instance = Dummy.new
@instance.stubs(:id).returns 123
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "uppercase.PNG"), 'rb')
styles = {:styles => { :large => ["400x400", :jpg],
:medium => ["100x100", :jpg],
:small => ["32x32#", :jpg]},
:default_style => :small}
@attachment = Paperclip::Attachment.new(:avatar,
@instance,
styles)
now = Time.now
Time.stubs(:now).returns(now)
@attachment.assign(@file)
@attachment.save
end
teardown do
@file.close
Paperclip::Attachment.default_options.merge!(@old_defaults)
end
should "should have matching to_s and url methods" do
file = @attachment.to_file
assert file
assert_match @attachment.to_s, @attachment.url
assert_match @attachment.to_s(:small), @attachment.url(:small)
file.close
end
end end
context "An attachment" do context "An attachment" do
...@@ -509,6 +754,15 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -509,6 +754,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")
...@@ -590,7 +844,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -590,7 +844,7 @@ class AttachmentTest < Test::Unit::TestCase
[:large, :medium, :small].each do |style| [:large, :medium, :small].each do |style|
io = @attachment.to_file(style) io = @attachment.to_file(style)
# p "in commit to disk test, io is #{io.inspect} and @instance.id is #{@instance.id}" # p "in commit to disk test, io is #{io.inspect} and @instance.id is #{@instance.id}"
assert File.exists?(io) assert File.exists?(io.path)
assert ! io.is_a?(::Tempfile) assert ! io.is_a?(::Tempfile)
io.close io.close
end end
...@@ -662,7 +916,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -662,7 +916,7 @@ class AttachmentTest < Test::Unit::TestCase
end end
should "not be able to find the module" do should "not be able to find the module" do
assert_raise(NameError){ Dummy.new.avatar } assert_raise(Paperclip::StorageMethodNotFound){ Dummy.new.avatar }
end end
end end
end end
...@@ -687,7 +941,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -687,7 +941,7 @@ class AttachmentTest < Test::Unit::TestCase
now = Time.now now = Time.now
Time.stubs(:now).returns(now) Time.stubs(:now).returns(now)
@dummy.avatar = @file @dummy.avatar = @file
assert now, @dummy.avatar.updated_at assert_equal now.to_i, @dummy.avatar.updated_at.to_i
end end
should "return nil when reloaded and sent #avatar_updated_at" do should "return nil when reloaded and sent #avatar_updated_at" do
...@@ -760,5 +1014,52 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -760,5 +1014,52 @@ class AttachmentTest < Test::Unit::TestCase
assert_equal @file.size, @dummy.avatar.size assert_equal @file.size, @dummy.avatar.size
end end
end end
context "and avatar_fingerprint column" do
setup do
ActiveRecord::Base.connection.add_column :dummies, :avatar_fingerprint, :string
rebuild_class
@dummy = Dummy.new
end
should "not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
should "return the right value when sent #avatar_fingerprint" do
@dummy.avatar = @file
assert_equal 'aec488126c3b33c08a10c3fa303acf27', @dummy.avatar_fingerprint
end
should "return the right value when saved, reloaded, and sent #avatar_fingerprint" do
@dummy.avatar = @file
@dummy.save
@dummy = Dummy.find(@dummy.id)
assert_equal 'aec488126c3b33c08a10c3fa303acf27', @dummy.avatar_fingerprint
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 dleete the file when model is destroy" do
@dummy.destroy
assert File.exists?(@path)
end
end
end end
require './test/helper'
require 'fog'
Fog.mock!
class FogTest < Test::Unit::TestCase
context "" do
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.instance_variable_get('@fog_public')
end
end
end
end
end
require 'test/helper' require './test/helper'
class GeometryTest < Test::Unit::TestCase class GeometryTest < Test::Unit::TestCase
context "Paperclip::Geometry" do context "Paperclip::Geometry" do
...@@ -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|
......
require 'rubygems' require 'rubygems'
require 'test/unit'
require 'shoulda'
require 'tempfile' require 'tempfile'
require 'test/unit'
gem 'jferris-mocha' require 'shoulda'
require 'mocha' require 'mocha'
gem 'sqlite3-ruby'
require 'active_record' require 'active_record'
require 'active_record/version'
require 'active_support' require 'active_support'
require 'mime/types'
puts "Testing against version #{ActiveRecord::VERSION::STRING}"
`ruby -e 'exit 0'` # Prime $? with a value.
begin begin
require 'ruby-debug' require 'ruby-debug'
rescue LoadError rescue LoadError => e
puts "ruby-debug not loaded" puts "debugger disabled"
end end
ROOT = File.join(File.dirname(__FILE__), '..') ROOT = File.join(File.dirname(__FILE__), '..')
RAILS_ROOT = ROOT
RAILS_ENV = "test" def silence_warnings
old_verbose, $VERBOSE = $VERBOSE, nil
yield
ensure
$VERBOSE = old_verbose
end
class Test::Unit::TestCase
def setup
silence_warnings do
Object.const_set(:Rails, stub('Rails', :root => ROOT, :env => 'test'))
end
end
end
$LOAD_PATH << File.join(ROOT, 'lib') $LOAD_PATH << File.join(ROOT, 'lib')
$LOAD_PATH << File.join(ROOT, 'lib', 'paperclip') $LOAD_PATH << File.join(ROOT, 'lib', 'paperclip')
require File.join(ROOT, 'lib', 'paperclip.rb') require File.join(ROOT, 'lib', 'paperclip.rb')
require 'shoulda_macros/paperclip' require './shoulda_macros/paperclip'
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures") 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 = Logger.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'])
def reset_class class_name def reset_class class_name
ActiveRecord::Base.send(:include, Paperclip) ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, class_name) rescue nil Object.send(:remove_const, class_name) rescue nil
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.class_eval{ include Paperclip } klass.class_eval{ include Paperclip::Glue }
klass klass
end end
...@@ -51,49 +68,43 @@ end ...@@ -51,49 +68,43 @@ 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
table.column :avatar_file_size, :integer table.column :avatar_file_size, :integer
table.column :avatar_updated_at, :datetime table.column :avatar_updated_at, :datetime
table.column :avatar_fingerprint, :string
end end
rebuild_class options rebuild_class options
end end
def rebuild_class options = {} def rebuild_class options = {}
ActiveRecord::Base.send(:include, Paperclip) 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))
Dummy.class_eval do Dummy.class_eval do
include Paperclip include Paperclip::Glue
has_attached_file :avatar, options has_attached_file :avatar, options
end end
end Dummy.reset_column_information
def temporary_rails_env(new_env)
old_env = Object.const_defined?("RAILS_ENV") ? RAILS_ENV : nil
silence_warnings do
Object.const_set("RAILS_ENV", new_env)
end
yield
silence_warnings do
Object.const_set("RAILS_ENV", old_env)
end
end end
class FakeModel class FakeModel
attr_accessor :avatar_file_name, attr_accessor :avatar_file_name,
:avatar_file_size, :avatar_file_size,
:avatar_last_updated, :avatar_updated_at,
:avatar_content_type, :avatar_content_type,
:avatar_fingerprint,
:id :id
def errors def errors
@errors ||= [] @errors ||= []
end end
def run_callbacks name, *args def run_paperclip_callbacks name, *args
end end
end end
def attachment options def attachment options
...@@ -106,3 +117,33 @@ def silence_warnings ...@@ -106,3 +117,33 @@ def silence_warnings
ensure ensure
$VERBOSE = old_verbose $VERBOSE = old_verbose
end end
def should_accept_dummy_class
should "accept the class" do
assert_accepts @matcher, @dummy_class
end
should "accept an instance of that class" do
assert_accepts @matcher, @dummy_class.new
end
end
def should_reject_dummy_class
should "reject the class" do
assert_rejects @matcher, @dummy_class
end
should "reject an instance of that class" do
assert_rejects @matcher, @dummy_class.new
end
end
def with_exitstatus_returning(code)
saved_exitstatus = $?.nil? ? 0 : $?.exitstatus
begin
`ruby -e 'exit #{code.to_i}'`
yield
ensure
`ruby -e 'exit #{saved_exitstatus.to_i}'`
end
end
require 'test/helper' require './test/helper'
class IntegrationTest < Test::Unit::TestCase class IntegrationTest < Test::Unit::TestCase
context "Many models at once" do context "Many models at once" do
...@@ -35,6 +35,22 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -35,6 +35,22 @@ class IntegrationTest < Test::Unit::TestCase
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
setup { File.chmod(0000, @dummy.avatar.path) }
should "not raise an error" do
assert_nothing_raised do
@dummy.avatar.reprocess!
end
end
should "return false" do
assert ! @dummy.avatar.reprocess!
end
teardown { File.chmod(0644, @dummy.avatar.path) }
end
context "redefining its attachment styles" do context "redefining its attachment styles" do
setup do setup do
Dummy.class_eval do Dummy.class_eval do
...@@ -42,6 +58,7 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -42,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
...@@ -50,6 +67,82 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -50,6 +67,82 @@ 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
end
end
context "Attachment" do
setup do
@thumb_path = "./test/../public/system/avatars/1/thumb/5k.png"
File.delete(@thumb_path) if File.exists?(@thumb_path)
rebuild_model :styles => { :thumb => "50x50#" }
@dummy = Dummy.new
@file = File.new(File.join(File.dirname(__FILE__),
"fixtures",
"5k.png"), 'rb')
end
teardown { @file.close }
should "not create the thumbnails upon saving when post-processing is disabled" do
@dummy.avatar.post_processing = false
@dummy.avatar = @file
assert @dummy.save
assert !File.exists?(@thumb_path)
end
should "create the thumbnails upon saving when post_processing is enabled" do
@dummy.avatar.post_processing = true
@dummy.avatar = @file
assert @dummy.save
assert File.exists?(@thumb_path)
end
end
context "Attachment with no generated thumbnails" do
setup do
@thumb_small_path = "./test/../public/system/avatars/1/thumb_small/5k.png"
@thumb_large_path = "./test/../public/system/avatars/1/thumb_large/5k.png"
File.delete(@thumb_small_path) if File.exists?(@thumb_small_path)
File.delete(@thumb_large_path) if File.exists?(@thumb_large_path)
rebuild_model :styles => { :thumb_small => "50x50#", :thumb_large => "60x60#" }
@dummy = Dummy.new
@file = File.new(File.join(File.dirname(__FILE__),
"fixtures",
"5k.png"), 'rb')
@dummy.avatar.post_processing = false
@dummy.avatar = @file
assert @dummy.save
@dummy.avatar.post_processing = true
end
teardown { @file.close }
should "allow us to create all thumbnails in one go" do
assert !File.exists?(@thumb_small_path)
assert !File.exists?(@thumb_large_path)
@dummy.avatar.reprocess!
assert File.exists?(@thumb_small_path)
assert File.exists?(@thumb_large_path)
end
should "allow us to selectively create each thumbnail" do
assert !File.exists?(@thumb_small_path)
assert !File.exists?(@thumb_large_path)
@dummy.avatar.reprocess! :thumb_small
assert File.exists?(@thumb_small_path)
assert !File.exists?(@thumb_large_path)
@dummy.avatar.reprocess! :thumb_large
assert File.exists?(@thumb_large_path)
end end
end end
...@@ -190,6 +283,38 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -190,6 +283,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>",
...@@ -204,7 +329,7 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -204,7 +329,7 @@ class IntegrationTest < Test::Unit::TestCase
@bad_file = File.new(File.join(FIXTURES_DIR, "bad.png"), 'rb') @bad_file = File.new(File.join(FIXTURES_DIR, "bad.png"), 'rb')
assert @dummy.avatar = @file assert @dummy.avatar = @file
assert @dummy.valid? assert @dummy.valid?, @dummy.errors.full_messages.join(", ")
assert @dummy.save assert @dummy.save
end end
...@@ -295,6 +420,24 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -295,6 +420,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
...@@ -311,6 +454,7 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -311,6 +454,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
...@@ -363,7 +507,6 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -363,7 +507,6 @@ class IntegrationTest < Test::Unit::TestCase
:thumb => ["32x32#", :gif] }, :thumb => ["32x32#", :gif] },
:storage => :s3, :storage => :s3,
:whiny_thumbnails => true, :whiny_thumbnails => true,
# :s3_options => {:logger => Logger.new(StringIO.new)},
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "s3.yml")), :s3_credentials => File.new(File.join(File.dirname(__FILE__), "s3.yml")),
:default_style => :medium, :default_style => :medium,
:bucket => ENV['S3_TEST_BUCKET'], :bucket => ENV['S3_TEST_BUCKET'],
...@@ -379,6 +522,18 @@ class IntegrationTest < Test::Unit::TestCase ...@@ -379,6 +522,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' require './test/helper'
class InterpolationsTest < Test::Unit::TestCase class InterpolationsTest < Test::Unit::TestCase
should "return all methods but the infrastructure when sent #all" do should "return all methods but the infrastructure when sent #all" do
...@@ -11,12 +11,12 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -11,12 +11,12 @@ class InterpolationsTest < Test::Unit::TestCase
end end
end end
should "return the RAILS_ROOT" do should "return the Rails.root" do
assert_equal RAILS_ROOT, Paperclip::Interpolations.rails_root(:attachment, :style) assert_equal Rails.root, Paperclip::Interpolations.rails_root(:attachment, :style)
end end
should "return the RAILS_ENV" do should "return the Rails.env" do
assert_equal RAILS_ENV, Paperclip::Interpolations.rails_env(:attachment, :style) assert_equal Rails.env, Paperclip::Interpolations.rails_env(:attachment, :style)
end end
should "return the class of the Interpolations module when called with no params" do should "return the class of the Interpolations module when called with no params" do
...@@ -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")
...@@ -82,14 +120,17 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -82,14 +120,17 @@ class InterpolationsTest < Test::Unit::TestCase
should "reinterpolate :url" do should "reinterpolate :url" do
attachment = mock attachment = mock
attachment.expects(:options).returns({:url => ":id"})
attachment.expects(:url).with(:style, false).returns("1234") attachment.expects(:url).with(:style, false).returns("1234")
assert_equal "1234", Paperclip::Interpolations.url(attachment, :style) assert_equal "1234", Paperclip::Interpolations.url(attachment, :style)
end end
should "raise if infinite loop detcted reinterpolating :url" do should "raise if infinite loop detcted reinterpolating :url" do
attachment = mock attachment = Object.new
attachment.expects(:options).returns({:url => ":url"}) class << attachment
def url(*args)
Paperclip::Interpolations.url(self, :style)
end
end
assert_raises(Paperclip::InfiniteInterpolationError){ Paperclip::Interpolations.url(attachment, :style) } assert_raises(Paperclip::InfiniteInterpolationError){ Paperclip::Interpolations.url(attachment, :style) }
end end
...@@ -107,11 +148,34 @@ class InterpolationsTest < Test::Unit::TestCase ...@@ -107,11 +148,34 @@ 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 timestamp" do should "return the timestamp" do
now = Time.now now = Time.now
zone = 'UTC'
attachment = mock attachment = mock
attachment.expects(:instance_read).with(:updated_at).returns(now) attachment.expects(:instance_read).with(:updated_at).returns(now)
assert_equal now.to_s, Paperclip::Interpolations.timestamp(attachment, :style) attachment.expects(:time_zone).returns(zone)
assert_equal now.in_time_zone(zone).to_s, Paperclip::Interpolations.timestamp(attachment, :style)
end
should "return updated_at" do
attachment = mock
seconds_since_epoch = 1234567890
attachment.expects(:updated_at).returns(seconds_since_epoch)
assert_equal seconds_since_epoch, Paperclip::Interpolations.updated_at(attachment, :style)
end
should "return hash" do
attachment = mock
fake_hash = "a_wicked_secure_hash"
attachment.expects(:hash).returns(fake_hash)
assert_equal fake_hash, Paperclip::Interpolations.hash(attachment, :style)
end end
should "call all expected interpolations with the given arguments" do should "call all expected interpolations with the given arguments" do
......
require 'test/helper' require './test/helper'
class IOStreamTest < Test::Unit::TestCase class IOStreamTest < Test::Unit::TestCase
context "IOStream" do include IOStream
should "be included in IO, File, Tempfile, and StringIO" do
[IO, File, Tempfile, StringIO].each do |klass|
assert klass.included_modules.include?(IOStream), "Not in #{klass}"
end
end
end
context "A file" do context "A file" 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')
...@@ -21,7 +14,7 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -21,7 +14,7 @@ class IOStreamTest < Test::Unit::TestCase
context "and given a String" do context "and given a String" do
setup do setup do
FileUtils.mkdir_p(File.join(ROOT, 'tmp')) FileUtils.mkdir_p(File.join(ROOT, 'tmp'))
assert @result = @file.stream_to(File.join(ROOT, 'tmp', 'iostream.string.test')) assert @result = stream_to(@file, File.join(ROOT, 'tmp', 'iostream.string.test'))
end end
should "return a File" do should "return a File" do
...@@ -38,7 +31,7 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -38,7 +31,7 @@ class IOStreamTest < Test::Unit::TestCase
setup do setup do
tempfile = Tempfile.new('iostream.test') tempfile = Tempfile.new('iostream.test')
tempfile.binmode tempfile.binmode
assert @result = @file.stream_to(tempfile) assert @result = stream_to(@file, tempfile)
end end
should "return a Tempfile" do should "return a Tempfile" do
...@@ -53,9 +46,9 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -53,9 +46,9 @@ class IOStreamTest < Test::Unit::TestCase
end end
context "that is sent #to_tempfile" do context "that is converted #to_tempfile" do
setup do setup do
assert @tempfile = @file.to_tempfile assert @tempfile = to_tempfile(@file)
end end
should "convert it to a Paperclip Tempfile" do should "convert it to a Paperclip Tempfile" do
...@@ -66,7 +59,7 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -66,7 +59,7 @@ class IOStreamTest < Test::Unit::TestCase
name = File.basename(@file.path) name = File.basename(@file.path)
extension = File.extname(name) extension = File.extname(name)
basename = File.basename(name, extension) basename = File.basename(name, extension)
assert_match %r[^#{Regexp.quote(basename)}.*?#{Regexp.quote(extension)}], File.basename(@tempfile.path) assert_match %r[^stream.*?#{Regexp.quote(extension)}], File.basename(@tempfile.path)
end end
should "have the Tempfile contain the same data as the file" do should "have the Tempfile contain the same data as the file" do
......
require 'test/helper' require './test/helper'
class HaveAttachedFileMatcherTest < Test::Unit::TestCase class HaveAttachedFileMatcherTest < Test::Unit::TestCase
context "have_attached_file" do context "have_attached_file" do
...@@ -8,14 +8,17 @@ class HaveAttachedFileMatcherTest < Test::Unit::TestCase ...@@ -8,14 +8,17 @@ class HaveAttachedFileMatcherTest < Test::Unit::TestCase
@matcher = self.class.have_attached_file(:avatar) @matcher = self.class.have_attached_file(:avatar)
end end
should "reject a class with no attachment" do context "given a class with no attachment" do
assert_rejects @matcher, @dummy_class should_reject_dummy_class
end end
should "accept a class with an attachment" do context "given a class with an attachment" do
setup do
modify_table("dummies"){|d| d.string :avatar_file_name } modify_table("dummies"){|d| d.string :avatar_file_name }
@dummy_class.has_attached_file :avatar @dummy_class.has_attached_file :avatar
assert_accepts @matcher, @dummy_class end
should_accept_dummy_class
end end
end end
end end
require 'test/helper' require './test/helper'
class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase
context "validate_attachment_content_type" do context "validate_attachment_content_type" do
setup do setup do
reset_table("dummies") do |d| reset_table("dummies") do |d|
d.string :title
d.string :avatar_file_name d.string :avatar_file_name
d.string :avatar_content_type d.string :avatar_content_type
end end
...@@ -14,18 +15,73 @@ class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase ...@@ -14,18 +15,73 @@ class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase
rejecting(%w(audio/mp3 application/octet-stream)) rejecting(%w(audio/mp3 application/octet-stream))
end end
should "reject a class with no validation" do context "given a class with no validation" do
assert_rejects @matcher, @dummy_class should_reject_dummy_class
end end
should "reject a class with a validation that doesn't match" do context "given a class with a validation that doesn't match" do
setup do
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{audio/.*}
end
should_reject_dummy_class
end
context "given a class with a matching validation" do
setup do
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
end
should_accept_dummy_class
end
context "given a class with other validations but matching types" do
setup do
@dummy_class.validates_presence_of :title
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
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/.*} @dummy_class.validates_attachment_content_type :avatar, :content_type => %r{audio/.*}
assert_rejects @matcher, @dummy_class @matcher = self.class.validate_attachment_content_type(:avatar).
allowing(%w(image/png image/jpeg))
end end
should "accept a class with a validation" do 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/.*} @dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
assert_accepts @matcher, @dummy_class @matcher = self.class.validate_attachment_content_type(:avatar).
rejecting(%w(audio/mp3 application/octet-stream))
end
should_accept_dummy_class
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 end
require 'test/helper' require './test/helper'
class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase
context "validate_attachment_presence" do context "validate_attachment_presence" do
...@@ -11,13 +11,16 @@ class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase ...@@ -11,13 +11,16 @@ class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase
@matcher = self.class.validate_attachment_presence(:avatar) @matcher = self.class.validate_attachment_presence(:avatar)
end end
should "reject a class with no validation" do context "given a class with no validation" do
assert_rejects @matcher, @dummy_class should_reject_dummy_class
end end
should "accept a class with a validation" do context "given a class with a matching validation" do
setup do
@dummy_class.validates_attachment_presence :avatar @dummy_class.validates_attachment_presence :avatar
assert_accepts @matcher, @dummy_class end
should_accept_dummy_class
end end
end end
end end
require 'test/helper' require './test/helper'
class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase
context "validate_attachment_size" do context "validate_attachment_size" do
...@@ -14,37 +14,37 @@ class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase ...@@ -14,37 +14,37 @@ class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase
context "of limited size" do context "of limited size" do
setup{ @matcher = self.class.validate_attachment_size(:avatar).in(256..1024) } setup{ @matcher = self.class.validate_attachment_size(:avatar).in(256..1024) }
should "reject a class with no validation" do context "given a class with no validation" do
assert_rejects @matcher, @dummy_class should_reject_dummy_class
end end
should "reject a class with a validation that's too high" do context "given a class with a validation that's too high" do
@dummy_class.validates_attachment_size :avatar, :in => 256..2048 setup { @dummy_class.validates_attachment_size :avatar, :in => 256..2048 }
assert_rejects @matcher, @dummy_class should_reject_dummy_class
end end
should "reject a class with a validation that's too low" do context "given a class with a validation that's too low" do
@dummy_class.validates_attachment_size :avatar, :in => 0..1024 setup { @dummy_class.validates_attachment_size :avatar, :in => 0..1024 }
assert_rejects @matcher, @dummy_class should_reject_dummy_class
end end
should "accept a class with a validation that matches" do context "given a class with a validation that matches" do
@dummy_class.validates_attachment_size :avatar, :in => 256..1024 setup { @dummy_class.validates_attachment_size :avatar, :in => 256..1024 }
assert_accepts @matcher, @dummy_class should_accept_dummy_class
end end
end end
context "validates_attachment_size with infinite range" do context "validates_attachment_size with infinite range" do
setup{ @matcher = self.class.validate_attachment_size(:avatar) } setup{ @matcher = self.class.validate_attachment_size(:avatar) }
should "accept a class with an upper limit" do context "given a class with an upper limit" do
@dummy_class.validates_attachment_size :avatar, :less_than => 1 setup { @dummy_class.validates_attachment_size :avatar, :less_than => 1 }
assert_accepts @matcher, @dummy_class should_accept_dummy_class
end end
should "accept a class with no upper limit" do context "given a class with no upper limit" do
@dummy_class.validates_attachment_size :avatar, :greater_than => 1 setup { @dummy_class.validates_attachment_size :avatar, :greater_than => 1 }
assert_accepts @matcher, @dummy_class should_accept_dummy_class
end end
end end
end end
......
require 'test/helper' require './test/helper'
class PaperclipTest < Test::Unit::TestCase class PaperclipTest < Test::Unit::TestCase
[:image_magick_path, :command_path].each do |path| context "Calling Paperclip.run" do
context "Calling Paperclip.run with #{path} specified" do should "run the command with Cocaine" do
setup do Cocaine::CommandLine.expects(:new).with("convert", "stuff").returns(stub(:run))
Paperclip.options[:image_magick_path] = nil Paperclip.run("convert", "stuff")
Paperclip.options[:command_path] = nil
Paperclip.options[path] = "/usr/bin"
end
should "return the expected path for path_for_command" do
assert_equal "/usr/bin/convert", Paperclip.path_for_command("convert")
end
should "execute the right command" do
Paperclip.expects(:path_for_command).with("convert").returns("/usr/bin/convert")
Paperclip.expects(:bit_bucket).returns("/dev/null")
Paperclip.expects(:"`").with("/usr/bin/convert one.jpg two.jpg 2>/dev/null")
Paperclip.run("convert", "one.jpg two.jpg")
end
end
end
context "Calling Paperclip.run with no path specified" do
setup do
Paperclip.options[:image_magick_path] = nil
Paperclip.options[:command_path] = nil
end
should "return the expected path fro path_for_command" do
assert_equal "convert", Paperclip.path_for_command("convert")
end
should "execute the right command" do
Paperclip.expects(:path_for_command).with("convert").returns("convert")
Paperclip.expects(:bit_bucket).returns("/dev/null")
Paperclip.expects(:"`").with("convert one.jpg two.jpg 2>/dev/null")
Paperclip.run("convert", "one.jpg two.jpg")
end
end
context "Calling Paperclip.run and logging" do
setup do
Paperclip.options[:image_magick_path] = nil
Paperclip.options[:command_path] = nil
Paperclip.stubs(:bit_bucket).returns("/dev/null")
Paperclip.stubs(:log)
Paperclip.stubs(:"`").with("this is the command 2>/dev/null")
end
should "log the command when :log_command is true" do
Paperclip.options[:log_command] = true
Paperclip.run("this","is the command")
assert_received(Paperclip, :log) do |p|
p.with("this is the command 2>/dev/null")
end
assert_received(Paperclip, :`) do |p|
p.with("this is the command 2>/dev/null")
end
end
should "not log the command when :log_command is false" do
Paperclip.options[:log_command] = false
Paperclip.run("this","is the command")
assert_received(Paperclip, :log) do |p|
p.with("this is the command 2>/dev/null").never
end
assert_received(Paperclip, :`) do |p|
p.with("this is the command 2>/dev/null")
end
end
end
context "Paperclip.bit_bucket" do
context "on systems without /dev/null" do
setup do
File.expects(:exists?).with("/dev/null").returns(false)
end
should "return 'NUL'" do
assert_equal "NUL", Paperclip.bit_bucket
end end
end end
context "on systems with /dev/null" do context "Paperclip.each_instance_with_attachment" do
setup do setup do
File.expects(:exists?).with("/dev/null").returns(true) @file = File.new(File.join(FIXTURES_DIR, "5k.png"), 'rb')
d1 = Dummy.create(:avatar => @file)
d2 = Dummy.create
d3 = Dummy.create(:avatar => @file)
@expected = [d1, d3]
end end
should "yield every instance of a model that has an attachment" do
should "return '/dev/null'" do actual = []
assert_equal "/dev/null", Paperclip.bit_bucket Paperclip.each_instance_with_attachment("Dummy", "avatar") do |instance|
actual << instance
end end
assert_same_elements @expected, actual
end end
end end
...@@ -106,6 +37,19 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -106,6 +37,19 @@ 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 "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,9 +167,16 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -223,9 +167,16 @@ class PaperclipTest < Test::Unit::TestCase
end end
end end
should "not have Attachment in the ActiveRecord::Base namespace" do
assert_raises(NameError) do
ActiveRecord::Base::Attachment
end
end
def self.should_validate validation, options, valid_file, invalid_file def self.should_validate validation, options, valid_file, invalid_file
context "with #{validation} validation and #{options.inspect} options" do context "with #{validation} validation and #{options.inspect} options" do
setup do setup do
rebuild_class
Dummy.send(:"validates_attachment_#{validation}", :avatar, options) Dummy.send(:"validates_attachment_#{validation}", :avatar, options)
@dummy = Dummy.new @dummy = Dummy.new
end end
...@@ -236,11 +187,11 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -236,11 +187,11 @@ class PaperclipTest < Test::Unit::TestCase
end end
if validation == :presence if validation == :presence
should "have an error on the attachment" do should "have an error on the attachment" do
assert @dummy.errors.on(:avatar_file_name) assert @dummy.errors[:avatar_file_name]
end end
else else
should "not have an error on the attachment" do should "not have an error on the attachment" do
assert_nil @dummy.errors.on(:avatar_file_name), @dummy.errors.full_messages.join(", ") assert @dummy.errors.blank?, @dummy.errors.full_messages.join(", ")
end end
end end
end end
...@@ -250,7 +201,7 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -250,7 +201,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
...@@ -259,7 +210,7 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -259,7 +210,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
...@@ -279,6 +230,21 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -279,6 +230,21 @@ class PaperclipTest < Test::Unit::TestCase
should_validate validation, options, valid_file, invalid_file should_validate validation, options, valid_file, invalid_file
end end
context "with content_type validation and lambda message" do
context "and assigned an invalid file" do
setup do
Dummy.send(:"validates_attachment_content_type", :avatar, :content_type => %r{image/.*}, :message => lambda {'lambda content type message'})
@dummy = Dummy.new
@dummy.avatar &&= File.open(File.join(FIXTURES_DIR, "text.txt"), "rb")
@dummy.valid?
end
should "have a content type error message" do
assert [@dummy.errors[:avatar_content_type]].flatten.any?{|error| error =~ %r/lambda content type message/ }
end
end
end
context "with size validation and less_than 10240 option" do context "with size validation and less_than 10240 option" do
context "and assigned an invalid file" do context "and assigned an invalid file" do
setup do setup do
...@@ -289,7 +255,22 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -289,7 +255,22 @@ class PaperclipTest < Test::Unit::TestCase
end end
should "have a file size min/max error message" do should "have a file size min/max error message" do
assert_match %r/between 0 and 10240 bytes/, @dummy.errors.on(:avatar_file_size) assert [@dummy.errors[:avatar_file_size]].flatten.any?{|error| error =~ %r/between 0 and 10240 bytes/ }
end
end
end
context "with size validation and less_than 10240 option with lambda message" do
context "and assigned an invalid file" do
setup do
Dummy.send(:"validates_attachment_size", :avatar, :less_than => 10240, :message => lambda {'lambda between 0 and 10240 bytes'})
@dummy = Dummy.new
@dummy.avatar &&= File.open(File.join(FIXTURES_DIR, "12k.png"), "rb")
@dummy.valid?
end
should "have a file size min/max error message" do
assert [@dummy.errors[:avatar_file_size]].flatten.any?{|error| error =~ %r/lambda between 0 and 10240 bytes/ }
end end
end end
end end
......
require 'test/helper' require './test/helper'
class ProcessorTest < Test::Unit::TestCase class ProcessorTest < Test::Unit::TestCase
should "instantiate and call #make when sent #make to the class" do should "instantiate and call #make when sent #make to the class" do
......
require 'test/helper' require './test/helper'
require 'aws/s3' require 'aws/s3'
class StorageTest < Test::Unit::TestCase class StorageTest < Test::Unit::TestCase
def rails_env(env) def rails_env(env)
silence_warnings do silence_warnings do
Object.const_set(:RAILS_ENV, env) Object.const_set(:Rails, stub('Rails', :env => env))
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
end end
...@@ -17,12 +40,6 @@ class StorageTest < Test::Unit::TestCase ...@@ -17,12 +40,6 @@ class StorageTest < Test::Unit::TestCase
@dummy = Dummy.new @dummy = Dummy.new
@avatar = @dummy.avatar @avatar = @dummy.avatar
@current_env = RAILS_ENV
end
teardown do
rails_env(@current_env)
end end
should "get the correct credentials when RAILS_ENV is production" do should "get the correct credentials when RAILS_ENV is production" do
...@@ -61,6 +78,49 @@ class StorageTest < Test::Unit::TestCase ...@@ -61,6 +78,49 @@ 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 "" do context "" do
setup do setup do
AWS::S3::Base.stubs(:establish_connection!) AWS::S3::Base.stubs(:establish_connection!)
...@@ -77,6 +137,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -77,6 +137,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!)
...@@ -97,7 +158,46 @@ class StorageTest < Test::Unit::TestCase ...@@ -97,7 +158,46 @@ 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 { |image| "cdn#{image.size.to_i % 4}.example.com" },
:path => ":attachment/:basename.:extension",
:url => ":s3_alias_url"
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".")
end
should "return a url based on the host_alias" do
assert_match %r{^http://cdn0.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,
...@@ -106,6 +206,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -106,6 +206,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"
...@@ -114,7 +215,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -114,7 +215,7 @@ 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 @dummy.avatar.expiring_url
end end
...@@ -124,6 +225,36 @@ class StorageTest < Test::Unit::TestCase ...@@ -124,6 +225,36 @@ class StorageTest < Test::Unit::TestCase
end end
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
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
should "should succeed" do
assert true
end
end
context "Parsing S3 credentials with a bucket in them" do context "Parsing S3 credentials with a bucket in them" do
setup do setup do
AWS::S3::Base.stubs(:establish_connection!) AWS::S3::Base.stubs(:establish_connection!)
...@@ -133,11 +264,8 @@ class StorageTest < Test::Unit::TestCase ...@@ -133,11 +264,8 @@ class StorageTest < Test::Unit::TestCase
:development => { :bucket => "dev_bucket" } :development => { :bucket => "dev_bucket" }
} }
@dummy = Dummy.new @dummy = Dummy.new
@old_env = RAILS_ENV
end end
teardown{ rails_env(@old_env) }
should "get the right bucket in production" do should "get the right bucket in production" do
rails_env("production") rails_env("production")
assert_equal "prod_bucket", @dummy.avatar.bucket_name assert_equal "prod_bucket", @dummy.avatar.bucket_name
...@@ -149,6 +277,33 @@ class StorageTest < Test::Unit::TestCase ...@@ -149,6 +277,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,
...@@ -194,6 +349,21 @@ class StorageTest < Test::Unit::TestCase ...@@ -194,6 +349,21 @@ class StorageTest < Test::Unit::TestCase
end end
end end
context "and saved without a bucket" do
setup do
class AWS::S3::NoSuchBucket < AWS::S3::ResponseError
# Force the class to be created as a proper subclass of ResponseError thanks to AWS::S3's autocreation of exceptions
end
AWS::S3::Bucket.expects(:create).with("testing")
AWS::S3::S3Object.stubs(:store).raises(AWS::S3::NoSuchBucket.new(:message, :response)).then.returns(true)
@dummy.save
end
should "succeed" do
assert true
end
end
context "and remove" do context "and remove" do
setup do setup do
AWS::S3::S3Object.stubs(:exists?).returns(true) AWS::S3::S3Object.stubs(:exists?).returns(true)
...@@ -263,6 +433,28 @@ class StorageTest < Test::Unit::TestCase ...@@ -263,6 +433,28 @@ class StorageTest < Test::Unit::TestCase
end end
end end
context "with S3 credentials supplied as Pathname" do
setup do
ENV['S3_KEY'] = 'pathname_key'
ENV['S3_BUCKET'] = 'pathname_bucket'
ENV['S3_SECRET'] = 'pathname_secret'
rails_env('test')
rebuild_model :storage => :s3,
:s3_credentials => Pathname.new(File.join(File.dirname(__FILE__))).join("fixtures/s3.yml")
Dummy.delete_all
@dummy = Dummy.new
end
should "parse the credentials" do
assert_equal 'pathname_bucket', @dummy.avatar.bucket_name
assert_equal 'pathname_key', AWS::S3::Base.connection.options[:access_key_id]
assert_equal 'pathname_secret', AWS::S3::Base.connection.options[:secret_access_key]
end
end
context "with S3 credentials in a YAML file" do context "with S3 credentials in a YAML file" do
setup do setup do
ENV['S3_KEY'] = 'env_key' ENV['S3_KEY'] = 'env_key'
...@@ -279,13 +471,139 @@ class StorageTest < Test::Unit::TestCase ...@@ -279,13 +471,139 @@ class StorageTest < Test::Unit::TestCase
@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
context "S3 Permissions" do
context "defaults to public-read" do
setup do
rebuild_model :storage => :s3,
:bucket => "testing",
:path => ":attachment/:style/:basename.:extension",
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
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!)
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
context "string permissions set" do
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
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!)
AWS::S3::S3Object.expects(:store).with(@dummy.avatar.path,
anything,
'testing',
:content_type => 'image/png',
:access => 'private')
@dummy.save
end
should "succeed" do
assert true
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 "succeed" do
assert true
end
end
end
end
end
unless ENV["S3_TEST_BUCKET"].blank? unless ENV["S3_TEST_BUCKET"].blank?
context "Using S3 for real, an attachment with S3 storage" do context "Using S3 for real, an attachment with S3 storage" do
setup do setup do
...@@ -312,7 +630,7 @@ class StorageTest < Test::Unit::TestCase ...@@ -312,7 +630,7 @@ class StorageTest < Test::Unit::TestCase
teardown { @file.close } teardown { @file.close }
should "still return a Tempfile when sent #to_file" do should "still return a Tempfile when sent #to_file" do
assert_equal Tempfile, @dummy.avatar.to_file.class assert_equal Paperclip::Tempfile, @dummy.avatar.to_file.class
end end
context "and saved" do context "and saved" do
...@@ -323,6 +641,11 @@ class StorageTest < Test::Unit::TestCase ...@@ -323,6 +641,11 @@ class StorageTest < Test::Unit::TestCase
should "be on S3" do should "be on S3" do
assert true assert true
end 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 end
end end
......
# encoding: utf-8 # encoding: utf-8
require 'test/helper' require './test/helper'
class StyleTest < Test::Unit::TestCase class StyleTest < Test::Unit::TestCase
...@@ -62,12 +62,12 @@ class StyleTest < Test::Unit::TestCase ...@@ -62,12 +62,12 @@ class StyleTest < Test::Unit::TestCase
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.styles
...@@ -92,6 +92,9 @@ class StyleTest < Test::Unit::TestCase ...@@ -92,6 +92,9 @@ class StyleTest < Test::Unit::TestCase
assert_nil @attachment.styles[:asstring].format assert_nil @attachment.styles[:asstring].format
end end
should "retain order" do
assert_equal [:aslist, :ashash, :asstring], @attachment.styles.keys
end
end end
context "An attachment with :convert_options" do context "An attachment with :convert_options" do
...@@ -114,6 +117,26 @@ class StyleTest < Test::Unit::TestCase ...@@ -114,6 +117,26 @@ class StyleTest < Test::Unit::TestCase
end end
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.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.styles[:thumb].source_file_options
end
end
context "A style rule with its own :processors" do context "A style rule with its own :processors" do
setup do setup do
@attachment = attachment :path => ":basename.:extension", @attachment = attachment :path => ":basename.:extension",
...@@ -138,4 +161,26 @@ class StyleTest < Test::Unit::TestCase ...@@ -138,4 +161,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.styles[:foo].instance_variable_get("@processors")
end
should "call procs when they are needed" do
assert_equal [:test], @attachment.styles[:foo].processors
end
end
end end
require 'test/helper' require './test/helper'
class ThumbnailTest < Test::Unit::TestCase class ThumbnailTest < Test::Unit::TestCase
context "A Paperclip Tempfile" do context "A Paperclip Tempfile" do
setup do setup do
@tempfile = Paperclip::Tempfile.new("file.jpg") @tempfile = Paperclip::Tempfile.new(["file", ".jpg"])
end end
should "have its path contain a real extension" do should "have its path contain a real extension" do
...@@ -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.expects(:"`").with do |arg| Paperclip.expects(:run).with do |*arg|
arg.match %r{convert\s+"#{File.expand_path(@thumb.file.path)}\[0\]"\s+-resize\s+\"x50\"\s+-crop\s+\"100x50\+114\+0\"\s+\+repage\s+".*?"} 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
...@@ -111,12 +129,14 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -111,12 +129,14 @@ class ThumbnailTest < Test::Unit::TestCase
end end
should "have source_file_options value set" do should "have source_file_options value set" do
assert_equal "-strip", @thumb.source_file_options assert_equal ["-strip"], @thumb.source_file_options
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.expects(:"`").with do |arg| Paperclip.expects(:run).with do |*arg|
arg.match %r{convert\s+-strip\s+"#{File.expand_path(@thumb.file.path)}\[0\]"\s+-resize\s+"x50"\s+-crop\s+"100x50\+114\+0"\s+\+repage\s+".*?"} 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
...@@ -149,12 +169,14 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -149,12 +169,14 @@ class ThumbnailTest < Test::Unit::TestCase
end end
should "have convert_options value set" do should "have convert_options value set" do
assert_equal "-strip -depth 8", @thumb.convert_options assert_equal %w"-strip -depth 8", @thumb.convert_options
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.expects(:"`").with do |arg| Paperclip.expects(:run).with do |*arg|
arg.match %r{convert\s+"#{File.expand_path(@thumb.file.path)}\[0\]"\s+-resize\s+"x50"\s+-crop\s+"100x50\+114\+0"\s+\+repage\s+-strip\s+-depth\s+8\s+".*?"} 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
...@@ -187,7 +221,7 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -187,7 +221,7 @@ class ThumbnailTest < Test::Unit::TestCase
end end
should "not get resized by default" do should "not get resized by default" do
assert_no_match(/-resize/, @thumb.transformation_command) assert !@thumb.transformation_command.include?("-resize")
end end
end end
end end
...@@ -224,4 +258,79 @@ class ThumbnailTest < Test::Unit::TestCase ...@@ -224,4 +258,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
require 'test/helper' require './test/helper'
class UpfileTest < Test::Unit::TestCase class UpfileTest < Test::Unit::TestCase
{ %w(jpg jpe jpeg) => 'image/jpeg', { %w(jpg jpe jpeg) => 'image/jpeg',
...@@ -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,12 @@ class UpfileTest < Test::Unit::TestCase ...@@ -33,4 +34,12 @@ class UpfileTest < Test::Unit::TestCase
end end
end end
end end
should "return a content_type of text/plain on a real file whose content_type is determined with the file command" do
file = File.new(File.join(File.dirname(__FILE__), "..", "LICENSE"))
class << file
include Paperclip::Upfile
end
assert_equal 'text/plain', file.content_type
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