mirror of
https://github.com/kemko/paperclip.git
synced 2026-01-01 16:05:40 +03:00
extract paperclip from insales to separate repository
This commit is contained in:
26
LICENSE
Normal file
26
LICENSE
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2008 Jon Yurek and thoughtbot, inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
174
README.rdoc
Normal file
174
README.rdoc
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
=Paperclip
|
||||||
|
|
||||||
|
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
|
||||||
|
much like other attributes as possible. This means they aren't saved to their
|
||||||
|
final locations on disk, nor are they deleted if set to nil, until
|
||||||
|
ActiveRecord::Base#save is called. It manages validations based on size and
|
||||||
|
presence, if required. It can transform its assigned image into thumbnails if
|
||||||
|
needed, and the prerequisites are as simple as installing ImageMagick (which,
|
||||||
|
for most modern Unix-based systems, is as easy as installing the right
|
||||||
|
packages). Attached files are saved to the filesystem and referenced in the
|
||||||
|
browser by an easily understandable specification, which has sensible and
|
||||||
|
useful defaults.
|
||||||
|
|
||||||
|
See the documentation for +has_attached_file+ in Paperclip::ClassMethods for
|
||||||
|
more detailed options.
|
||||||
|
|
||||||
|
==Quick Start
|
||||||
|
|
||||||
|
In your model:
|
||||||
|
|
||||||
|
class User < ActiveRecord::Base
|
||||||
|
has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }
|
||||||
|
end
|
||||||
|
|
||||||
|
In your migrations:
|
||||||
|
|
||||||
|
class AddAvatarColumnsToUser < ActiveRecord::Migration
|
||||||
|
def self.up
|
||||||
|
add_column :users, :avatar_file_name, :string
|
||||||
|
add_column :users, :avatar_content_type, :string
|
||||||
|
add_column :users, :avatar_file_size, :integer
|
||||||
|
add_column :users, :avatar_updated_at, :datetime
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
remove_column :users, :avatar_file_name
|
||||||
|
remove_column :users, :avatar_content_type
|
||||||
|
remove_column :users, :avatar_file_size
|
||||||
|
remove_column :users, :avatar_updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
In your edit and new views:
|
||||||
|
|
||||||
|
<% form_for :user, @user, :url => user_path, :html => { :multipart => true } do |form| %>
|
||||||
|
<%= form.file_field :avatar %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
In your controller:
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user = User.create( params[:user] )
|
||||||
|
end
|
||||||
|
|
||||||
|
In your show view:
|
||||||
|
|
||||||
|
<%= image_tag @user.avatar.url %>
|
||||||
|
<%= image_tag @user.avatar.url(:medium) %>
|
||||||
|
<%= image_tag @user.avatar.url(:thumb) %>
|
||||||
|
|
||||||
|
==Usage
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
friendly front end. The attributes are <attachment>_file_name,
|
||||||
|
<attachment>_file_size, <attachment>_content_type, and <attachment>_updated_at.
|
||||||
|
Only <attachment>_file_name is required for paperclip to operate. More
|
||||||
|
information about the options to has_attached_file is available in the
|
||||||
|
documentation of Paperclip::ClassMethods.
|
||||||
|
|
||||||
|
Attachments can be validated with Paperclip's validation methods,
|
||||||
|
validates_attachment_presence, validates_attachment_content_type, and
|
||||||
|
validates_attachment_size.
|
||||||
|
|
||||||
|
==Storage
|
||||||
|
|
||||||
|
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
|
||||||
|
location is ":rails_root/public/system/:attachment/:id/:style/:filename". This
|
||||||
|
location was chosen because on standard Capistrano deployments, the
|
||||||
|
public/system directory is symlinked to the app's shared directory, meaning it
|
||||||
|
will survive between deployments. For example, using that :path, you may have a
|
||||||
|
file at
|
||||||
|
|
||||||
|
/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
|
||||||
|
safer choice for the default file store.
|
||||||
|
|
||||||
|
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
|
||||||
|
Paperclip::Storage::S3.
|
||||||
|
|
||||||
|
Files on the local filesystem (and in the Rails app's public directory) will be
|
||||||
|
available to the internet at large. If you require access control, it's
|
||||||
|
possible to place your files in a different location. You will need to change
|
||||||
|
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
|
||||||
|
variables.
|
||||||
|
|
||||||
|
==Post Processing
|
||||||
|
|
||||||
|
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
|
||||||
|
"styles" are actually "thumbnails". However, you can do much more than just
|
||||||
|
thumbnail images. By defining a subclass of Paperclip::Processor, you can
|
||||||
|
perform any processing you want on the files that are attached. Any file in
|
||||||
|
your Rails app's lib/paperclip_processors directory is automatically loaded by
|
||||||
|
paperclip, allowing you to easily define custom processors. You can specify a
|
||||||
|
processor with the :processors option to has_attached_file:
|
||||||
|
|
||||||
|
has_attached_file :scan, :styles => { :text => { :quality => :better } },
|
||||||
|
:processors => [:ocr]
|
||||||
|
|
||||||
|
This would load the hypothetical class Paperclip::Ocr, which would have the
|
||||||
|
hash "{ :quality => :better }" passed to it along with the uploaded file. For
|
||||||
|
more information about defining processors, see Paperclip::Processor.
|
||||||
|
|
||||||
|
The default processor is Paperclip::Thumbnail. For backwards compatability
|
||||||
|
reasons, you can pass a single geometry string or an array containing a
|
||||||
|
geometry and a format, which the file will be converted to, like so:
|
||||||
|
|
||||||
|
has_attached_file :avatar, :styles => { :thumb => ["32x32#", :png] }
|
||||||
|
|
||||||
|
This will convert the "thumb" style to a 32x32 square in png format, regardless
|
||||||
|
of what was uploaded. If the format is not specified, it is kept the same (i.e.
|
||||||
|
jpgs will remain jpgs).
|
||||||
|
|
||||||
|
Multiple processors can be specified, and they will be invoked in the order
|
||||||
|
they are defined in the :processors array. Each successive processor will
|
||||||
|
be given the result of the previous processor's execution. All processors will
|
||||||
|
receive the same parameters, which are what you define in the :styles hash.
|
||||||
|
For example, assuming we had this definition:
|
||||||
|
|
||||||
|
has_attached_file :scan, :styles => { :text => { :quality => :better } },
|
||||||
|
:processors => [:rotator, :ocr]
|
||||||
|
|
||||||
|
then both the :rotator processor and the :ocr processor would receive the
|
||||||
|
options "{ :quality => :better }". This parameter may not mean anything to one
|
||||||
|
or more or the processors, and they are expected to ignore it.
|
||||||
|
|
||||||
|
NOTE: Because processors operate by turning the original attachment into the
|
||||||
|
styles, no processors will be run if there are no styles defined.
|
||||||
|
|
||||||
|
==Events
|
||||||
|
|
||||||
|
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
|
||||||
|
step. The callbacks are "before_post_process" and "after_post_process" (which
|
||||||
|
are called before and after the processing of each attachment), and the
|
||||||
|
attachment-specific "before_<attachment>_post_process" and
|
||||||
|
"after_<attachment>_post_process". The callbacks are intended to be as close to
|
||||||
|
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
|
||||||
|
will halt. Returning false in an after_ filter will not halt anything, but you
|
||||||
|
can access the model and the attachment if necessary.
|
||||||
|
|
||||||
|
NOTE: Post processing will not even *start* if the attachment is not valid
|
||||||
|
according to the validations. Your callbacks and processors will *only* be
|
||||||
|
called with valid attachments.
|
||||||
|
|
||||||
|
==Contributing
|
||||||
|
|
||||||
|
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
|
||||||
|
guidelines:
|
||||||
|
|
||||||
|
1. Ask on the mailing list, or post a new GitHub Issue.
|
||||||
|
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
|
||||||
|
about writing tests for paperclip, please ask the mailing list.
|
||||||
99
Rakefile
Normal file
99
Rakefile
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
require 'rake'
|
||||||
|
require 'rake/testtask'
|
||||||
|
require 'rake/rdoctask'
|
||||||
|
|
||||||
|
$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
|
||||||
|
require 'paperclip'
|
||||||
|
|
||||||
|
desc 'Default: run unit tests.'
|
||||||
|
task :default => [:clean, :test]
|
||||||
|
|
||||||
|
desc 'Test the paperclip plugin.'
|
||||||
|
Rake::TestTask.new(:test) do |t|
|
||||||
|
t.libs << 'lib' << 'profile'
|
||||||
|
t.pattern = 'test/**/*_test.rb'
|
||||||
|
t.verbose = true
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Start an IRB session with all necessary files required.'
|
||||||
|
task :shell do |t|
|
||||||
|
chdir File.dirname(__FILE__)
|
||||||
|
exec 'irb -I lib/ -I lib/paperclip -r rubygems -r active_record -r tempfile -r init'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Generate documentation for the paperclip plugin.'
|
||||||
|
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||||
|
rdoc.rdoc_dir = 'doc'
|
||||||
|
rdoc.title = 'Paperclip'
|
||||||
|
rdoc.options << '--line-numbers' << '--inline-source'
|
||||||
|
rdoc.rdoc_files.include('README*')
|
||||||
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Update documentation on website'
|
||||||
|
task :sync_docs => 'rdoc' do
|
||||||
|
`rsync -ave ssh doc/ dev@dev.thoughtbot.com:/home/dev/www/dev.thoughtbot.com/paperclip`
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Clean up files.'
|
||||||
|
task :clean do |t|
|
||||||
|
FileUtils.rm_rf "doc"
|
||||||
|
FileUtils.rm_rf "tmp"
|
||||||
|
FileUtils.rm_rf "pkg"
|
||||||
|
FileUtils.rm "test/debug.log" rescue nil
|
||||||
|
FileUtils.rm "test/paperclip.db" rescue nil
|
||||||
|
Dir.glob("paperclip-*.gem").each{|f| FileUtils.rm f }
|
||||||
|
end
|
||||||
|
|
||||||
|
include_file_globs = ["README*",
|
||||||
|
"LICENSE",
|
||||||
|
"Rakefile",
|
||||||
|
"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 'thoughtbot-shoulda'
|
||||||
|
s.add_development_dependency 'mocha'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Print a list of the files to be put into the gem"
|
||||||
|
task :manifest => :clean do
|
||||||
|
spec.files.each do |file|
|
||||||
|
puts file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Generate a gemspec file for GitHub"
|
||||||
|
task :gemspec => :clean do
|
||||||
|
File.open("#{spec.name}.gemspec", 'w') do |f|
|
||||||
|
f.write spec.to_ruby
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Build the gem into the current directory"
|
||||||
|
task :gem => :gemspec do
|
||||||
|
`gem build #{spec.name}.gemspec`
|
||||||
|
end
|
||||||
5
generators/paperclip/USAGE
Normal file
5
generators/paperclip/USAGE
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Usage:
|
||||||
|
|
||||||
|
script/generate paperclip Class attachment1 (attachment2 ...)
|
||||||
|
|
||||||
|
This will create a migration that will add the proper columns to your class's table.
|
||||||
27
generators/paperclip/paperclip_generator.rb
Normal file
27
generators/paperclip/paperclip_generator.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
class PaperclipGenerator < Rails::Generator::NamedBase
|
||||||
|
attr_accessor :attachments, :migration_name
|
||||||
|
|
||||||
|
def initialize(args, options = {})
|
||||||
|
super
|
||||||
|
@class_name, @attachments = args[0], args[1..-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def manifest
|
||||||
|
file_name = generate_file_name
|
||||||
|
@migration_name = file_name.camelize
|
||||||
|
record do |m|
|
||||||
|
m.migration_template "paperclip_migration.rb.erb",
|
||||||
|
File.join('db', 'migrate'),
|
||||||
|
:migration_file_name => file_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_file_name
|
||||||
|
names = attachments.map{|a| a.underscore }
|
||||||
|
names = names[0..-2] + ["and", names[-1]] if names.length > 1
|
||||||
|
"add_attachments_#{names.join("_")}_to_#{@class_name.underscore}"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
19
generators/paperclip/templates/paperclip_migration.rb.erb
Normal file
19
generators/paperclip/templates/paperclip_migration.rb.erb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
class <%= migration_name %> < ActiveRecord::Migration
|
||||||
|
def self.up
|
||||||
|
<% attachments.each do |attachment| -%>
|
||||||
|
add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_name, :string
|
||||||
|
add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_content_type, :string
|
||||||
|
add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_size, :integer
|
||||||
|
add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_updated_at, :datetime
|
||||||
|
<% end -%>
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
<% attachments.each do |attachment| -%>
|
||||||
|
remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_name
|
||||||
|
remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_content_type
|
||||||
|
remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_size
|
||||||
|
remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_updated_at
|
||||||
|
<% end -%>
|
||||||
|
end
|
||||||
|
end
|
||||||
1
init.rb
Normal file
1
init.rb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
require File.join(File.dirname(__FILE__), "lib", "paperclip")
|
||||||
369
lib/paperclip.rb
Normal file
369
lib/paperclip.rb
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# Paperclip allows file attachments that are stored in the filesystem. All graphical
|
||||||
|
# transformations are done using the Graphics/ImageMagick command line utilities and
|
||||||
|
# are stored in Tempfiles until the record is saved. Paperclip does not require a
|
||||||
|
# separate model for storing the attachment's information, instead adding a few simple
|
||||||
|
# columns to your table.
|
||||||
|
#
|
||||||
|
# Author:: Jon Yurek
|
||||||
|
# Copyright:: Copyright (c) 2008-2009 thoughtbot, inc.
|
||||||
|
# License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||||
|
#
|
||||||
|
# Paperclip defines an attachment as any file, though it makes special considerations
|
||||||
|
# for image files. You can declare that a model has an attached file with the
|
||||||
|
# +has_attached_file+ method:
|
||||||
|
#
|
||||||
|
# class User < ActiveRecord::Base
|
||||||
|
# has_attached_file :avatar, :styles => { :thumb => "100x100" }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# user = User.new
|
||||||
|
# user.avatar = params[:user][:avatar]
|
||||||
|
# user.avatar.url
|
||||||
|
# # => "/users/avatars/4/original_me.jpg"
|
||||||
|
# user.avatar.url(:thumb)
|
||||||
|
# # => "/users/avatars/4/thumb_me.jpg"
|
||||||
|
#
|
||||||
|
# See the +has_attached_file+ documentation for more details.
|
||||||
|
|
||||||
|
require 'tempfile'
|
||||||
|
require 'fastimage'
|
||||||
|
require 'paperclip/upfile'
|
||||||
|
require 'paperclip/iostream'
|
||||||
|
require 'paperclip/geometry'
|
||||||
|
require 'paperclip/processor'
|
||||||
|
require 'paperclip/thumbnail'
|
||||||
|
require 'paperclip/storage'
|
||||||
|
require 'paperclip/interpolations'
|
||||||
|
require 'paperclip/attachment'
|
||||||
|
if defined? Rails.root
|
||||||
|
Dir.glob(File.join(File.expand_path(Rails.root), "lib", "paperclip_processors", "*.rb")).each do |processor|
|
||||||
|
require processor
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The base module that gets included in ActiveRecord::Base. See the
|
||||||
|
# documentation for Paperclip::ClassMethods for more useful information.
|
||||||
|
module Paperclip
|
||||||
|
|
||||||
|
VERSION = "2.2.9.2"
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# Provides configurability to Paperclip. There are a number of options available, such as:
|
||||||
|
# * whiny: Will raise an error if Paperclip cannot process thumbnails of
|
||||||
|
# an uploaded image. Defaults to true.
|
||||||
|
# * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
|
||||||
|
# log levels, etc. Defaults to true.
|
||||||
|
# * command_path: Defines the path at which to find the command line
|
||||||
|
# programs if they are not visible to Rails the system's search path. Defaults to
|
||||||
|
# nil, which uses the first executable found in the user's search path.
|
||||||
|
# * image_magick_path: Deprecated alias of command_path.
|
||||||
|
def options
|
||||||
|
@options ||= {
|
||||||
|
:whiny => true,
|
||||||
|
:image_magick_path => nil,
|
||||||
|
:command_path => nil,
|
||||||
|
:log => true,
|
||||||
|
:log_command => false,
|
||||||
|
:swallow_stderr => true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def path_for_command command #:nodoc:
|
||||||
|
if options[:image_magick_path]
|
||||||
|
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
|
||||||
|
|
||||||
|
def interpolates key, &block
|
||||||
|
Paperclip::Interpolations[key] = block
|
||||||
|
end
|
||||||
|
|
||||||
|
# The run method takes a command to execute and a string of parameters
|
||||||
|
# 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
|
||||||
|
# they are in different paths, the suggested course of action is to
|
||||||
|
# symlink them so they are all in the same directory.
|
||||||
|
#
|
||||||
|
# If the command returns with a result code that is not one of the
|
||||||
|
# expected_outcodes, a PaperclipCommandLineError will be raised. Generally
|
||||||
|
# a code of 0 is expected, but a list of codes may be passed if necessary.
|
||||||
|
#
|
||||||
|
# This method can log the command being run when
|
||||||
|
# Paperclip.options[:log_command] is set to true (defaults to false). This
|
||||||
|
# will only log if logging in general is set to true as well.
|
||||||
|
def run cmd, params = "", expected_outcodes = 0
|
||||||
|
command = %Q<#{%Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ")}>
|
||||||
|
command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr]
|
||||||
|
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
|
||||||
|
base.class_attribute :attachment_definitions
|
||||||
|
|
||||||
|
if base.respond_to?(:set_callback)
|
||||||
|
base.send :include, Paperclip::CallbackCompatability::Rails3
|
||||||
|
else
|
||||||
|
base.send :include, Paperclip::CallbackCompatability::Rails21
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def processor name #:nodoc:
|
||||||
|
name = name.to_s.camelize
|
||||||
|
processor = Paperclip.const_get(name)
|
||||||
|
unless processor.ancestors.include?(Paperclip::Processor)
|
||||||
|
raise PaperclipError.new("Processor #{name} was not found")
|
||||||
|
end
|
||||||
|
processor
|
||||||
|
end
|
||||||
|
|
||||||
|
# Log a paperclip-specific line. Uses ActiveRecord::Base.logger
|
||||||
|
# by default. Set Paperclip.options[:log] to false to turn off.
|
||||||
|
def log message
|
||||||
|
logger.info("[paperclip] #{message}") if logging?
|
||||||
|
end
|
||||||
|
|
||||||
|
def logger #:nodoc:
|
||||||
|
ActiveRecord::Base.logger
|
||||||
|
end
|
||||||
|
|
||||||
|
def logging? #:nodoc:
|
||||||
|
options[:log]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PaperclipError < StandardError #:nodoc:
|
||||||
|
end
|
||||||
|
|
||||||
|
class PaperclipCommandLineError < StandardError #:nodoc:
|
||||||
|
end
|
||||||
|
|
||||||
|
class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
|
||||||
|
end
|
||||||
|
|
||||||
|
class InfiniteInterpolationError < PaperclipError #:nodoc:
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
# +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.
|
||||||
|
# The attribute returns a Paperclip::Attachment object which handles the management of
|
||||||
|
# that file. The intent is to make the attachment as much like a normal attribute. The
|
||||||
|
# thumbnails will be created when the new file is assigned, but they will *not* be saved
|
||||||
|
# until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
|
||||||
|
# called on it, the attachment will *not* be deleted until +save+ is called. See the
|
||||||
|
# Paperclip::Attachment documentation for more specifics. There are a number of options
|
||||||
|
# you can set to change the behavior of a Paperclip attachment:
|
||||||
|
# * +url+: The full URL of where the attachment is publically accessible. This can just
|
||||||
|
# as easily point to a directory served directly through Apache as it can to an action
|
||||||
|
# that can control permissions. You can specify the full domain and path, but usually
|
||||||
|
# just an absolute path is sufficient. The leading slash *must* be included manually for
|
||||||
|
# absolute paths. The default value is
|
||||||
|
# "/system/:attachment/:id/:style/:filename". See
|
||||||
|
# Paperclip::Attachment#interpolate for more information on variable interpolaton.
|
||||||
|
# :url => "/:class/:attachment/:id/:style_:filename"
|
||||||
|
# :url => "http://some.other.host/stuff/:class/:id_:extension"
|
||||||
|
# * +default_url+: The URL that will be returned if there is no attachment assigned.
|
||||||
|
# This field is interpolated just as the url is. The default value is
|
||||||
|
# "/:attachment/:style/missing.png"
|
||||||
|
# has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
|
||||||
|
# User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
|
||||||
|
# * +styles+: A hash of thumbnail styles and their geometries. You can find more about
|
||||||
|
# geometry strings at the ImageMagick website
|
||||||
|
# (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
|
||||||
|
# also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
|
||||||
|
# inside the dimensions and then crop the rest off (weighted at the center). The
|
||||||
|
# default value is to generate no thumbnails.
|
||||||
|
# * +default_style+: The thumbnail style that will be used by default URLs.
|
||||||
|
# Defaults to +original+.
|
||||||
|
# has_attached_file :avatar, :styles => { :normal => "100x100#" },
|
||||||
|
# :default_style => :normal
|
||||||
|
# user.avatar.url # => "/avatars/23/normal_me.png"
|
||||||
|
# * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
|
||||||
|
# to a command line error. This will override the global setting for this attachment.
|
||||||
|
# Defaults to true. This option used to be called :whiny_thumbanils, but this is
|
||||||
|
# deprecated.
|
||||||
|
# * +convert_options+: When creating thumbnails, use this free-form options
|
||||||
|
# field 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
|
||||||
|
# "-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)
|
||||||
|
# Note that this option takes a hash of options, each of which correspond to the style
|
||||||
|
# of thumbnail being generated. You can also specify :all as a key, which will apply
|
||||||
|
# to all of the thumbnails being generated. If you specify options for the :original,
|
||||||
|
# it would be best if you did not specify destructive options, as the intent of keeping
|
||||||
|
# the original around is to regenerate all the thumbnails when requirements change.
|
||||||
|
# has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
|
||||||
|
# :convert_options => {
|
||||||
|
# :all => "-strip",
|
||||||
|
# :negative => "-negate"
|
||||||
|
# }
|
||||||
|
# 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
|
||||||
|
# :styles for compatability with future versions.
|
||||||
|
# * +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
|
||||||
|
# documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
|
||||||
|
# for backend-specific options.
|
||||||
|
def has_attached_file name, options = {}
|
||||||
|
include InstanceMethods
|
||||||
|
|
||||||
|
if attachment_definitions.nil?
|
||||||
|
self.attachment_definitions = {}
|
||||||
|
else
|
||||||
|
self.attachment_definitions = self.attachment_definitions.dup
|
||||||
|
end
|
||||||
|
attachment_definitions[name] = {:validations => []}.merge(options)
|
||||||
|
|
||||||
|
after_save :save_attached_files
|
||||||
|
after_commit :destroy_attached_files, on: :destroy
|
||||||
|
after_commit :flush_attachment_jobs
|
||||||
|
|
||||||
|
define_paperclip_callbacks :post_process, :"#{name}_post_process"
|
||||||
|
|
||||||
|
define_method name do |*args|
|
||||||
|
a = attachment_for(name)
|
||||||
|
(args.length > 0) ? a.to_s(args.first) : a
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method "#{name}=" do |file|
|
||||||
|
attachment_for(name).assign(file)
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method "#{name}?" do
|
||||||
|
attachment_for(name).file?
|
||||||
|
end
|
||||||
|
|
||||||
|
validates_each(name) do |record, attr, value|
|
||||||
|
attachment = record.attachment_for(name)
|
||||||
|
attachment.send(:flush_errors) unless attachment.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Places ActiveRecord-style validations on the size of the file assigned. The
|
||||||
|
# possible options are:
|
||||||
|
# * +in+: a Range of bytes (i.e. +1..1.megabyte+),
|
||||||
|
# * +less_than+: equivalent to :in => 0..options[:less_than]
|
||||||
|
# * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
|
||||||
|
# * +message+: error message to display, use :min and :max as replacements
|
||||||
|
# * +if+: A lambda or name of a method on the instance. Validation will only
|
||||||
|
# be run is this lambda or method returns true.
|
||||||
|
# * +unless+: Same as +if+ but validates if lambda or method returns false.
|
||||||
|
def validates_attachment_size name, options = {}
|
||||||
|
min = options[:greater_than] || (options[:in] && options[:in].first) || 0
|
||||||
|
max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
|
||||||
|
range = (min..max)
|
||||||
|
message = options[:message] || "file size must be between :min and :max bytes."
|
||||||
|
|
||||||
|
attachment_definitions[name][:validations] << [:size, {:range => range,
|
||||||
|
:message => message,
|
||||||
|
:if => options[:if],
|
||||||
|
:unless => options[:unless]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
|
||||||
|
def validates_attachment_thumbnails name, options = {}
|
||||||
|
warn('[DEPRECATION] validates_attachment_thumbnail is deprecated. ' +
|
||||||
|
'This validation is on by default and will be removed from future versions. ' +
|
||||||
|
'If you wish to turn it off, supply :whiny => false in your definition.')
|
||||||
|
attachment_definitions[name][:whiny_thumbnails] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Places ActiveRecord-style validations on the presence of a file.
|
||||||
|
# Options:
|
||||||
|
# * +if+: A lambda or name of a method on the instance. Validation will only
|
||||||
|
# be run is this lambda or method returns true.
|
||||||
|
# * +unless+: Same as +if+ but validates if lambda or method returns false.
|
||||||
|
def validates_attachment_presence name, options = {}
|
||||||
|
message = options[:message] || :blank
|
||||||
|
|
||||||
|
attachment_definitions[name][:validations] << [:presence, {:message => message,
|
||||||
|
:if => options[:if],
|
||||||
|
:unless => options[:unless]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Places ActiveRecord-style validations on the content type of the file
|
||||||
|
# assigned. The possible options are:
|
||||||
|
# * +content_type+: Allowed content types. Can be a single content type
|
||||||
|
# or an array. Each type can be a String or a Regexp. It should be
|
||||||
|
# noted that Internet Explorer upload files with content_types that you
|
||||||
|
# may not expect. For example, JPEG images are given image/pjpeg and
|
||||||
|
# PNGs are image/x-png, so keep that in mind when determining how you
|
||||||
|
# match. Allows all by default.
|
||||||
|
# * +message+: The message to display when the uploaded file has an invalid
|
||||||
|
# content type.
|
||||||
|
# * +if+: A lambda or name of a method on the instance. Validation will only
|
||||||
|
# be run is this lambda or method returns true.
|
||||||
|
# * +unless+: Same as +if+ but validates if lambda or method returns false.
|
||||||
|
# NOTE: If you do not specify an [attachment]_content_type field on your
|
||||||
|
# model, content_type validation will work _ONLY upon assignment_ and
|
||||||
|
# re-validation after the instance has been reloaded will always succeed.
|
||||||
|
def validates_attachment_content_type name, options = {}
|
||||||
|
attachment_definitions[name][:validations] << [:content_type, {:content_type => options[:content_type],
|
||||||
|
:message => options[:message],
|
||||||
|
:if => options[:if],
|
||||||
|
:unless => options[:unless]}]
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachment_definitions
|
||||||
|
self.attachment_definitions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module InstanceMethods #:nodoc:
|
||||||
|
def attachment_for name
|
||||||
|
@_paperclip_attachments ||= {}
|
||||||
|
@_paperclip_attachments[name] ||= if self.class.attachment_definitions[name][:storage] == :delayeds3
|
||||||
|
DelayedS3Attachment.new(name, self, self.class.attachment_definitions[name])
|
||||||
|
else
|
||||||
|
Attachment.new(name, self, self.class.attachment_definitions[name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def each_attachment
|
||||||
|
self.class.attachment_definitions.each do |name, definition|
|
||||||
|
yield(name, attachment_for(name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_attached_files
|
||||||
|
logger.info("[paperclip] Saving attachments.")
|
||||||
|
each_attachment do |name, attachment|
|
||||||
|
attachment.send(:save)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_attached_files
|
||||||
|
logger.info("[paperclip] Deleting attachments.")
|
||||||
|
each_attachment do |name, attachment|
|
||||||
|
attachment.send(:queue_existing_for_delete)
|
||||||
|
attachment.send(:flush_deletes)
|
||||||
|
end
|
||||||
|
logger.info("[paperclip] Finish deleting attachments.")
|
||||||
|
end
|
||||||
|
|
||||||
|
def flush_attachment_jobs
|
||||||
|
logger.info("[paperclip] flushing jobs.")
|
||||||
|
each_attachment do |name, attachment|
|
||||||
|
attachment.send(:flush_jobs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set it all up.
|
||||||
|
if Object.const_defined?("ActiveRecord")
|
||||||
|
ActiveRecord::Base.send(:include, Paperclip)
|
||||||
|
File.send(:include, Paperclip::Upfile)
|
||||||
|
end
|
||||||
533
lib/paperclip/attachment.rb
Normal file
533
lib/paperclip/attachment.rb
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
module Paperclip
|
||||||
|
# The Attachment class manages the files for a given attachment. It saves
|
||||||
|
# when the model saves, deletes when the model is destroyed, and processes
|
||||||
|
# the file upon assignment.
|
||||||
|
class Attachment
|
||||||
|
include IOStream
|
||||||
|
|
||||||
|
MAX_FILE_NAME_LENGTH = 100
|
||||||
|
MAX_IMAGE_RESOLUTION = 8192
|
||||||
|
|
||||||
|
def self.default_options
|
||||||
|
@default_options ||= {
|
||||||
|
:url => "/system/:attachment/:id/:style/:filename",
|
||||||
|
:path => ":rails_root/public:url",
|
||||||
|
:style_order => [],
|
||||||
|
:styles => {},
|
||||||
|
:default_url => "/:attachment/:style/missing.png",
|
||||||
|
:default_style => :original,
|
||||||
|
:validations => [],
|
||||||
|
:storage => :filesystem,
|
||||||
|
# :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
|
||||||
|
:whiny => true,
|
||||||
|
:restricted_characters => /[^\w\p{Word}\d\.\-]|(^\.{0,2}$)+/,
|
||||||
|
:filename_sanitizer => nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :name, :instance, :style_order, :styles, :default_style,
|
||||||
|
:convert_options, :queued_for_write, :options
|
||||||
|
|
||||||
|
attr_accessor :post_processing
|
||||||
|
|
||||||
|
# Creates an Attachment object. +name+ is the name of the attachment,
|
||||||
|
# +instance+ is the ActiveRecord object instance it's attached to, and
|
||||||
|
# +options+ is the same as the hash passed to +has_attached_file+.
|
||||||
|
def initialize name, instance, options = {}
|
||||||
|
@name = name
|
||||||
|
@instance = instance
|
||||||
|
|
||||||
|
options = self.class.default_options.merge(options)
|
||||||
|
|
||||||
|
@url = options[:url]
|
||||||
|
@s3_url = options[:s3_url]
|
||||||
|
@s3_path = options[:s3_path]
|
||||||
|
@filesystem_url = options[:filesystem_url]
|
||||||
|
@filesystem_path = options[:filesystem_path]
|
||||||
|
@url = @url.call(self) if @url.is_a?(Proc)
|
||||||
|
@path = options[:path]
|
||||||
|
@path = @path.call(self) if @path.is_a?(Proc)
|
||||||
|
@style_order = options[:style_order]
|
||||||
|
@style_order = @style_order.call(self) if @style_order.is_a?(Proc)
|
||||||
|
@styles = options[:styles]
|
||||||
|
@styles = @styles.call(self) if @styles.is_a?(Proc)
|
||||||
|
@default_url = options[:default_url]
|
||||||
|
@filesystem_url = options[:filesystem_url]
|
||||||
|
@validations = options[:validations]
|
||||||
|
@default_style = options[:default_style]
|
||||||
|
@storage = options[:storage]
|
||||||
|
@whiny = options[:whiny_thumbnails] || options[:whiny]
|
||||||
|
@convert_options = options[:convert_options] || {}
|
||||||
|
@processors = options[:processors] || [:thumbnail]
|
||||||
|
@options = options
|
||||||
|
@queued_for_delete = []
|
||||||
|
@queued_for_write = {}
|
||||||
|
@queued_jobs = []
|
||||||
|
@errors = {}
|
||||||
|
@validation_errors = nil
|
||||||
|
@dirty = false
|
||||||
|
|
||||||
|
@post_processing = true
|
||||||
|
@processing_url = options[:processing_url] || @default_url
|
||||||
|
|
||||||
|
normalize_style_definition
|
||||||
|
initialize_storage
|
||||||
|
end
|
||||||
|
|
||||||
|
# What gets called when you call instance.attachment = File. It clears
|
||||||
|
# errors, assigns attributes, processes the file, and runs validations. It
|
||||||
|
# also queues up the previous file for deletion, to be flushed away on
|
||||||
|
# #save of its host. In addition to form uploads, you can also assign
|
||||||
|
# another Paperclip attachment:
|
||||||
|
# new_user.avatar = old_user.avatar
|
||||||
|
# If the file that is assigned is not valid, the processing (i.e.
|
||||||
|
# thumbnailing, etc) will NOT be run.
|
||||||
|
def assign uploaded_file
|
||||||
|
ensure_required_accessors!
|
||||||
|
|
||||||
|
# загрузка через nginx
|
||||||
|
if uploaded_file.is_a?(Hash) && uploaded_file.has_key?('original_name')
|
||||||
|
u = uploaded_file
|
||||||
|
uploaded_file = FastUploadFile.new(uploaded_file)
|
||||||
|
log "fast upload: #{u.inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if uploaded_file.is_a?(Paperclip::Attachment)
|
||||||
|
uploaded_file = uploaded_file.to_file(:original)
|
||||||
|
close_uploaded_file = uploaded_file.respond_to?(:close)
|
||||||
|
end
|
||||||
|
|
||||||
|
if image_content_type?(uploaded_file) && !valid_image_resolution?(uploaded_file)
|
||||||
|
@errors[:base] = :too_large
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return unless valid_assignment?(uploaded_file)
|
||||||
|
|
||||||
|
uploaded_file.binmode if uploaded_file.respond_to? :binmode
|
||||||
|
self.clear
|
||||||
|
|
||||||
|
return if uploaded_file.nil?
|
||||||
|
|
||||||
|
@queued_for_write[:original] = to_tempfile(uploaded_file)
|
||||||
|
|
||||||
|
file_name = if @options[:filename_sanitizer]
|
||||||
|
@options[:filename_sanitizer].call uploaded_file.original_filename, self
|
||||||
|
else
|
||||||
|
sanitize_filename uploaded_file.original_filename
|
||||||
|
end
|
||||||
|
|
||||||
|
instance_write(:file_name, file_name)
|
||||||
|
instance_write(:content_type, uploaded_file.content_type.to_s.strip)
|
||||||
|
instance_write(:file_size, uploaded_file.size.to_i)
|
||||||
|
instance_write(:updated_at, Time.now)
|
||||||
|
|
||||||
|
@dirty = true
|
||||||
|
|
||||||
|
post_process if post_processing && valid?
|
||||||
|
|
||||||
|
updater = :"#{name}_file_name_will_change!"
|
||||||
|
instance.send updater if instance.respond_to? updater
|
||||||
|
# Reset the file size if the original file was reprocessed.
|
||||||
|
instance_write(:file_size, @queued_for_write[:original].size.to_i)
|
||||||
|
ensure
|
||||||
|
uploaded_file.close if close_uploaded_file
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_content_type?(file)
|
||||||
|
file.respond_to?(:content_type) && file.content_type.try(:include?, 'image')
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_image_resolution? file
|
||||||
|
sizes = FastImage.size(file)
|
||||||
|
!sizes || (sizes[0] <= MAX_IMAGE_RESOLUTION && sizes[1] <= MAX_IMAGE_RESOLUTION)
|
||||||
|
end
|
||||||
|
|
||||||
|
def most_appropriate_url
|
||||||
|
# stub for delayed_paperclip
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the public URL of the attachment, with a given style. Note that
|
||||||
|
# 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
|
||||||
|
# grained security. This is not recommended if you don't need the
|
||||||
|
# security, however, for performance reasons. set
|
||||||
|
# include_updated_timestamp to false if you want to stop the attachment
|
||||||
|
# update time appended to the url
|
||||||
|
def url style = default_style, include_updated_timestamp = true
|
||||||
|
u = @url
|
||||||
|
if @storage == :delayeds3
|
||||||
|
u = instance_read(:synced_to_s3) ? options[:s3_url] : options[:filesystem_url]
|
||||||
|
end
|
||||||
|
# for delayed_paperclip
|
||||||
|
if @instance.respond_to?("#{name}_processing?") && @instance.send("#{name}_processing?")
|
||||||
|
return interpolate @processing_url, style
|
||||||
|
end
|
||||||
|
url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(u, style)
|
||||||
|
include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
|
||||||
|
end
|
||||||
|
|
||||||
|
# Метод необходим в ассетах
|
||||||
|
def filesystem_url style = default_style, include_updated_timestamp = true
|
||||||
|
u = @url
|
||||||
|
u = options[:filesystem_url] if @storage == :delayeds3
|
||||||
|
url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(u, style)
|
||||||
|
include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the path of the attachment as defined by the :path option. If the
|
||||||
|
# file is stored in the filesystem the path refers to the path of the file
|
||||||
|
# on disk. If the file is stored in S3, the path is the "key" part of the
|
||||||
|
# URL, and the :bucket option refers to the S3 bucket.
|
||||||
|
def path style = default_style
|
||||||
|
return if original_filename.nil?
|
||||||
|
p = @path
|
||||||
|
if @storage == :delayeds3
|
||||||
|
p = instance_read(:synced_to_s3) ? options[:s3_path] : options[:filesystem_path]
|
||||||
|
end
|
||||||
|
interpolate(p, style)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filesystem_path style = default_style
|
||||||
|
return if original_filename.nil?
|
||||||
|
p = @path
|
||||||
|
p = options[:filesystem_path] if @storage == :delayeds3
|
||||||
|
interpolate(p, style)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Alias to +url+
|
||||||
|
def to_s style = nil
|
||||||
|
url(style)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if there are no errors on this attachment.
|
||||||
|
def valid?
|
||||||
|
validate
|
||||||
|
errors.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns an array containing the errors on this attachment.
|
||||||
|
def errors
|
||||||
|
@errors
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if there are changes that need to be saved.
|
||||||
|
def dirty?
|
||||||
|
@dirty
|
||||||
|
end
|
||||||
|
|
||||||
|
# Saves the file, if there are no errors. If there are, it flushes them to
|
||||||
|
# the instance's errors and returns false, cancelling the save.
|
||||||
|
def save
|
||||||
|
if valid?
|
||||||
|
flush_deletes
|
||||||
|
flush_writes
|
||||||
|
@dirty = false
|
||||||
|
true
|
||||||
|
else
|
||||||
|
flush_errors
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clears out the attachment. Has the same effect as previously assigning
|
||||||
|
# nil to the attachment. Does NOT save. If you wish to clear AND save,
|
||||||
|
# use #destroy.
|
||||||
|
def clear
|
||||||
|
queue_existing_for_delete
|
||||||
|
@errors = {}
|
||||||
|
@validation_errors = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Destroys the attachment. Has the same effect as previously assigning
|
||||||
|
# nil to the attachment *and saving*. This is permanent. If you wish to
|
||||||
|
# wipe out the existing attachment but not save, use #clear.
|
||||||
|
def destroy
|
||||||
|
clear
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the name of the file as originally assigned, and lives in the
|
||||||
|
# <attachment>_file_name attribute of the model.
|
||||||
|
def original_filename
|
||||||
|
instance_read(:file_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the size of the file as originally assigned, and lives in the
|
||||||
|
# <attachment>_file_size attribute of the model.
|
||||||
|
def size
|
||||||
|
instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the content_type of the file as originally assigned, and lives
|
||||||
|
# in the <attachment>_content_type attribute of the model.
|
||||||
|
def content_type
|
||||||
|
instance_read(:content_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the last modified time of the file as originally assigned, and
|
||||||
|
# lives in the <attachment>_updated_at attribute of the model.
|
||||||
|
def updated_at
|
||||||
|
time = instance_read(:updated_at)
|
||||||
|
time && time.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def cdn_domain
|
||||||
|
instance_read(:cdn_domain).try(:domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cdn_protocol
|
||||||
|
domain = instance_read(:cdn_domain)
|
||||||
|
if domain
|
||||||
|
if domain.ssl_configured?
|
||||||
|
'https'
|
||||||
|
else
|
||||||
|
'http'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
'https'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Paths and URLs can have a number of variables interpolated into them
|
||||||
|
# to vary the storage location based on name, id, style, class, etc.
|
||||||
|
# This method is a deprecated access into supplying and retrieving these
|
||||||
|
# interpolations. Future access should use either Paperclip.interpolates
|
||||||
|
# or extend the Paperclip::Interpolations module directly.
|
||||||
|
def self.interpolations
|
||||||
|
warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' +
|
||||||
|
'and will be removed from future versions. ' +
|
||||||
|
'Use Paperclip.interpolates instead')
|
||||||
|
Paperclip::Interpolations
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_filename(file_name)
|
||||||
|
file_name = file_name.strip
|
||||||
|
file_name.gsub!(@options[:restricted_characters], '_') if @options[:restricted_characters]
|
||||||
|
|
||||||
|
# Укорачиваем слишком длинные имена файлов.
|
||||||
|
if file_name.length > MAX_FILE_NAME_LENGTH
|
||||||
|
# 1 символ имени и точка и того - 2
|
||||||
|
ext = file_name.match(/\.(\w{0,#{MAX_FILE_NAME_LENGTH - 2}})$/) ? $1 : ""
|
||||||
|
# 1 - из-за того что отсчет идет от 0, и еще 1 из-за точки, и того 2
|
||||||
|
file_name = file_name[0..(MAX_FILE_NAME_LENGTH - 2 - ext.length)]
|
||||||
|
file_name << ".#{ext}" if !ext.blank?
|
||||||
|
end
|
||||||
|
file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method really shouldn't be called that often. It's expected use is
|
||||||
|
# in the paperclip:refresh rake task and that's it. It will regenerate all
|
||||||
|
# thumbnails forcefully, by reobtaining the original file and going through
|
||||||
|
# the post-process again.
|
||||||
|
def reprocess!
|
||||||
|
new_original = Tempfile.new("paperclip-reprocess-#{instance.id}-")
|
||||||
|
new_original.binmode
|
||||||
|
old_original = to_file(:original)
|
||||||
|
new_original.write( old_original.read )
|
||||||
|
new_original.rewind
|
||||||
|
@queued_for_write = { :original => new_original }
|
||||||
|
post_process
|
||||||
|
old_original.close if old_original.respond_to?(:close)
|
||||||
|
save
|
||||||
|
flush_jobs
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if a file has been assigned.
|
||||||
|
def file?
|
||||||
|
!original_filename.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Writes the attachment-specific attribute on the instance. For example,
|
||||||
|
# instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
|
||||||
|
# "avatar_file_name" field (assuming the attachment is called avatar).
|
||||||
|
def instance_write(attr, value)
|
||||||
|
setter = :"#{name}_#{attr}="
|
||||||
|
responds = instance.respond_to?(setter)
|
||||||
|
self.instance_variable_set("@_#{setter.to_s.chop}", value)
|
||||||
|
instance.send(setter, value) if responds || attr.to_s == "file_name"
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance_update(attr, value)
|
||||||
|
setter = :"#{name}_#{attr}="
|
||||||
|
responds = instance.respond_to?(setter)
|
||||||
|
self.instance_variable_set("@_#{setter.to_s.chop}", value)
|
||||||
|
instance.update_attribute(:"#{name}_#{attr}", value) if responds
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Reads the attachment-specific attribute on the instance. See instance_write
|
||||||
|
# for more details.
|
||||||
|
def instance_read(attr)
|
||||||
|
getter = :"#{name}_#{attr}"
|
||||||
|
responds = instance.respond_to?(getter)
|
||||||
|
cached = self.instance_variable_get("@_#{getter}")
|
||||||
|
return cached if cached
|
||||||
|
instance.send(getter) if responds || attr.to_s == "file_name"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_required_accessors! #:nodoc:
|
||||||
|
%w(file_name).each do |field|
|
||||||
|
unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
|
||||||
|
raise PaperclipError.new("#{@instance.class} model missing required attr_accessor for '#{name}_#{field}'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def log message #:nodoc:
|
||||||
|
Paperclip.log(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_assignment? file #:nodoc:
|
||||||
|
file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate #:nodoc:
|
||||||
|
unless @validation_errors
|
||||||
|
@validation_errors = @validations.inject({}) do |errors, validation|
|
||||||
|
name, options = validation
|
||||||
|
errors[name] = send(:"validate_#{name}", options) if allow_validation?(options)
|
||||||
|
errors
|
||||||
|
end
|
||||||
|
@validation_errors.reject!{|k,v| v == nil }
|
||||||
|
@errors.merge!(@validation_errors)
|
||||||
|
end
|
||||||
|
@validation_errors
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_validation? options #:nodoc:
|
||||||
|
(options[:if].nil? || check_guard(options[:if])) && (options[:unless].nil? || !check_guard(options[:unless]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_guard guard #:nodoc:
|
||||||
|
if guard.respond_to? :call
|
||||||
|
guard.call(instance)
|
||||||
|
elsif ! guard.blank?
|
||||||
|
instance.send(guard.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_size options #:nodoc:
|
||||||
|
if file? && !options[:range].include?(size.to_i)
|
||||||
|
options[:message]#.gsub(/:min/, options[:min].to_s).gsub(/:max/, options[:max].to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_presence options #:nodoc:
|
||||||
|
options[:message] unless file?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_content_type options #:nodoc:
|
||||||
|
valid_types = [options[:content_type]].flatten
|
||||||
|
unless original_filename.blank?
|
||||||
|
unless valid_types.blank?
|
||||||
|
content_type = instance_read(:content_type)
|
||||||
|
unless valid_types.any?{|t| content_type.nil? || t === content_type }
|
||||||
|
options[:message] || "is not one of the allowed file types."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_style_definition #:nodoc:
|
||||||
|
styles.each do |name, args|
|
||||||
|
unless args.is_a? Hash
|
||||||
|
dimensions, format = [args, nil].flatten[0..1]
|
||||||
|
format = nil if format.blank?
|
||||||
|
@styles[name] = {
|
||||||
|
:processors => @processors,
|
||||||
|
:geometry => dimensions,
|
||||||
|
:format => format,
|
||||||
|
:whiny => @whiny,
|
||||||
|
:convert_options => extra_options_for(name)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
@styles[name] = {
|
||||||
|
:processors => @processors,
|
||||||
|
:whiny => @whiny,
|
||||||
|
:convert_options => extra_options_for(name)
|
||||||
|
}.merge(@styles[name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def solidify_style_definitions #:nodoc:
|
||||||
|
@styles.each do |name, args|
|
||||||
|
@styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call)
|
||||||
|
@styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_storage #:nodoc:
|
||||||
|
@storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
|
||||||
|
self.extend(@storage_module)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extra_options_for(style) #:nodoc:
|
||||||
|
all_options = convert_options[:all]
|
||||||
|
all_options = all_options.call(instance) if all_options.respond_to?(:call)
|
||||||
|
style_options = convert_options[style]
|
||||||
|
style_options = style_options.call(instance) if style_options.respond_to?(:call)
|
||||||
|
|
||||||
|
[ style_options, all_options ].compact.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_process #:nodoc:
|
||||||
|
return unless content_type.match(/image/)
|
||||||
|
return if @queued_for_write[:original].nil?
|
||||||
|
solidify_style_definitions
|
||||||
|
|
||||||
|
instance.run_paperclip_callbacks(:post_process) do
|
||||||
|
instance.run_paperclip_callbacks(:"#{name}_post_process") do
|
||||||
|
post_process_styles
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_process_styles #:nodoc:
|
||||||
|
styles_in_order = @style_order.empty? ? @styles : @styles.sort_by{|s| @style_order.index(s.first)}
|
||||||
|
styles_in_order.each do |name, args|
|
||||||
|
begin
|
||||||
|
raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank?
|
||||||
|
@queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor|
|
||||||
|
Paperclip.processor(processor).make(file, args, self)
|
||||||
|
end
|
||||||
|
rescue PaperclipError => e
|
||||||
|
log("An error was received while processing: #{e.inspect}")
|
||||||
|
(@errors[:processing] ||= []) << e.message if @whiny
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def interpolate pattern, style = default_style #:nodoc:
|
||||||
|
Paperclip::Interpolations.interpolate(pattern, self, style)
|
||||||
|
end
|
||||||
|
|
||||||
|
def queue_existing_for_delete #:nodoc:
|
||||||
|
return unless file?
|
||||||
|
@queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
|
||||||
|
@storage == :delayeds3 ? filesystem_path(style) : path(style)
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def flush_errors #:nodoc:
|
||||||
|
@errors.each do |error, message|
|
||||||
|
[message].flatten.each {|m| instance.errors.add(name, m) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def flush_jobs
|
||||||
|
@queued_jobs.try(:each, &:call)
|
||||||
|
@queued_jobs = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
class DelayedS3Attachment < Attachment
|
||||||
|
include Paperclip::Storage::Delayeds3
|
||||||
|
extend Paperclip::Storage::Delayeds3::ClassMethods
|
||||||
|
end
|
||||||
|
end
|
||||||
62
lib/paperclip/callback_compatability.rb
Normal file
62
lib/paperclip/callback_compatability.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
|
|
||||||
115
lib/paperclip/geometry.rb
Normal file
115
lib/paperclip/geometry.rb
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
module Paperclip
|
||||||
|
|
||||||
|
# Defines the geometry of an image.
|
||||||
|
class Geometry
|
||||||
|
attr_accessor :height, :width, :modifier
|
||||||
|
|
||||||
|
# Gives a Geometry representing the given height and width
|
||||||
|
def initialize width = nil, height = nil, modifier = nil
|
||||||
|
@height = height.to_f
|
||||||
|
@width = width.to_f
|
||||||
|
@modifier = modifier
|
||||||
|
end
|
||||||
|
|
||||||
|
# Uses ImageMagick to determing the dimensions of a file, passed in as either a
|
||||||
|
# File or path.
|
||||||
|
def self.from_file file
|
||||||
|
file = file.path if file.respond_to? "path"
|
||||||
|
geometry = begin
|
||||||
|
Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]])
|
||||||
|
rescue PaperclipCommandLineError => e
|
||||||
|
""
|
||||||
|
end
|
||||||
|
parse(geometry) ||
|
||||||
|
raise(NotIdentifiedByImageMagickError.new("Формат файла не соответствует его расширению."))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parses a "WxH" formatted string, where W is the width and H is the height.
|
||||||
|
def self.parse string
|
||||||
|
if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
|
||||||
|
Geometry.new(*match[1,3])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# True if the dimensions represent a square
|
||||||
|
def square?
|
||||||
|
height == width
|
||||||
|
end
|
||||||
|
|
||||||
|
# True if the dimensions represent a horizontal rectangle
|
||||||
|
def horizontal?
|
||||||
|
height < width
|
||||||
|
end
|
||||||
|
|
||||||
|
# True if the dimensions represent a vertical rectangle
|
||||||
|
def vertical?
|
||||||
|
height > width
|
||||||
|
end
|
||||||
|
|
||||||
|
# The aspect ratio of the dimensions.
|
||||||
|
def aspect
|
||||||
|
width / height
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the larger of the two dimensions
|
||||||
|
def larger
|
||||||
|
[height, width].max
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the smaller of the two dimensions
|
||||||
|
def smaller
|
||||||
|
[height, width].min
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the width and height in a format suitable to be passed to Geometry.parse
|
||||||
|
def to_s
|
||||||
|
s = ""
|
||||||
|
s << width.to_i.to_s if width > 0
|
||||||
|
s << "x#{height.to_i}" if height > 0
|
||||||
|
s << modifier.to_s
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
# Same as to_s
|
||||||
|
def inspect
|
||||||
|
to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the scaling and cropping geometries (in string-based ImageMagick format)
|
||||||
|
# neccessary to transform this Geometry into the Geometry given. If crop is true,
|
||||||
|
# then it is assumed the destination Geometry will be the exact final resolution.
|
||||||
|
# In this case, the source Geometry is scaled so that an image containing the
|
||||||
|
# destination Geometry would be completely filled by the source image, and any
|
||||||
|
# overhanging image would be cropped. Useful for square thumbnail images. The cropping
|
||||||
|
# is weighted at the center of the Geometry.
|
||||||
|
def transformation_to dst, crop = false
|
||||||
|
if crop
|
||||||
|
ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
|
||||||
|
scale_geometry, scale = scaling(dst, ratio)
|
||||||
|
crop_geometry = cropping(dst, ratio, scale)
|
||||||
|
else
|
||||||
|
scale_geometry = dst.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
[ scale_geometry, crop_geometry ]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scaling dst, ratio
|
||||||
|
if ratio.horizontal? || ratio.square?
|
||||||
|
[ "%dx" % dst.width, ratio.width ]
|
||||||
|
else
|
||||||
|
[ "x%d" % dst.height, ratio.height ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cropping dst, ratio, scale
|
||||||
|
if ratio.horizontal? || ratio.square?
|
||||||
|
"%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
|
||||||
|
else
|
||||||
|
"%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
105
lib/paperclip/interpolations.rb
Normal file
105
lib/paperclip/interpolations.rb
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
module Paperclip
|
||||||
|
# This module contains all the methods that are available for interpolation
|
||||||
|
# in paths and urls. To add your own (or override an existing one), you
|
||||||
|
# can either open this module and define it, or call the
|
||||||
|
# Paperclip.interpolates method.
|
||||||
|
module Interpolations
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# Hash assignment of interpolations. Included only for compatability,
|
||||||
|
# and is not intended for normal use.
|
||||||
|
def self.[]= name, block
|
||||||
|
define_method(name, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Hash access of interpolations. Included only for compatability,
|
||||||
|
# and is not intended for normal use.
|
||||||
|
def self.[] name
|
||||||
|
method(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a sorted list of all interpolations.
|
||||||
|
def self.all
|
||||||
|
self.instance_methods(false).sort
|
||||||
|
end
|
||||||
|
|
||||||
|
# Perform the actual interpolation. Takes the pattern to interpolate
|
||||||
|
# and the arguments to pass, which are the attachment and style name.
|
||||||
|
def self.interpolate pattern, *args
|
||||||
|
all.reverse.inject( pattern.dup ) do |result, tag|
|
||||||
|
result.gsub(/:#{tag}/) do |match|
|
||||||
|
send( tag, *args )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the filename, the same way as ":basename.:extension" would.
|
||||||
|
def filename attachment, style
|
||||||
|
"#{basename(attachment, style)}.#{extension(attachment, style)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the interpolated URL. Will raise an error if the url itself
|
||||||
|
# contains ":url" to prevent infinite recursion. This interpolation
|
||||||
|
# is used in the default :path to ease default specifications.
|
||||||
|
def url attachment, style
|
||||||
|
raise InfiniteInterpolationError if attachment.options[:url].include?(":url")
|
||||||
|
attachment.url(style, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the timestamp as defined by the <attachment>_updated_at field
|
||||||
|
def timestamp attachment, style
|
||||||
|
attachment.instance_read(:updated_at).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the Rails.root constant.
|
||||||
|
def rails_root attachment, style
|
||||||
|
Rails.root
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the Rails.env constant.
|
||||||
|
def rails_env attachment, style
|
||||||
|
Rails.env
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the underscored, pluralized version of the class name.
|
||||||
|
# e.g. "users" for the User class.
|
||||||
|
def class attachment, style
|
||||||
|
attachment.instance.class.to_s.underscore.pluralize
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the basename of the file. e.g. "file" for "file.jpg"
|
||||||
|
def basename attachment, style
|
||||||
|
attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the extension of the file. e.g. "jpg" for "file.jpg"
|
||||||
|
# If the style has a format defined, it will return the format instead
|
||||||
|
# of the actual extension.
|
||||||
|
def extension attachment, style
|
||||||
|
((style = attachment.styles[style]) && style[:format]) ||
|
||||||
|
File.extname(attachment.original_filename).gsub(/^\.+/, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the id of the instance.
|
||||||
|
def id attachment, style
|
||||||
|
attachment.instance.id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the id of the instance in a split path form. e.g. returns
|
||||||
|
# 000/001/234 for an id of 1234.
|
||||||
|
def id_partition attachment, style
|
||||||
|
("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the pluralized form of the attachment name. e.g.
|
||||||
|
# "avatars" for an attachment of :avatar
|
||||||
|
def attachment attachment, style
|
||||||
|
attachment.name.to_s.downcase.pluralize
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the style, or the default style if nil is supplied.
|
||||||
|
def style attachment, style
|
||||||
|
style || attachment.default_style
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
45
lib/paperclip/iostream.rb
Normal file
45
lib/paperclip/iostream.rb
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
|
||||||
|
# and Tempfile conversion.
|
||||||
|
module IOStream
|
||||||
|
# Returns a Tempfile containing the contents of the readable object.
|
||||||
|
def to_tempfile(object)
|
||||||
|
return object.to_tempfile if object.respond_to?(:to_tempfile)
|
||||||
|
name = object.respond_to?(:original_filename) ? object.original_filename : (object.respond_to?(:path) ? object.path : "stream")
|
||||||
|
tempfile = Tempfile.new(["stream", File.extname(name)])
|
||||||
|
tempfile.binmode
|
||||||
|
stream_to(object, tempfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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. Returns a File if a String is passed
|
||||||
|
# in as the destination and returns the IO or Tempfile as passed in if one is sent as the destination.
|
||||||
|
def stream_to object, path_or_file, in_blocks_of = 8192
|
||||||
|
dstio = case path_or_file
|
||||||
|
when String then File.new(path_or_file, "wb+")
|
||||||
|
when IO then path_or_file
|
||||||
|
when Tempfile then path_or_file
|
||||||
|
end
|
||||||
|
buffer = ""
|
||||||
|
object.rewind
|
||||||
|
while object.read(in_blocks_of, buffer) do
|
||||||
|
dstio.write(buffer)
|
||||||
|
end
|
||||||
|
dstio.rewind
|
||||||
|
dstio
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Corrects a bug in Windows when asking for Tempfile size.
|
||||||
|
if defined?(Tempfile) && RUBY_PLATFORM !~ /java/
|
||||||
|
class Tempfile
|
||||||
|
def size
|
||||||
|
if @tmpfile
|
||||||
|
@tmpfile.fsync
|
||||||
|
@tmpfile.flush
|
||||||
|
@tmpfile.stat.size
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
4
lib/paperclip/matchers.rb
Normal file
4
lib/paperclip/matchers.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
require 'paperclip/matchers/have_attached_file_matcher'
|
||||||
|
require 'paperclip/matchers/validate_attachment_presence_matcher'
|
||||||
|
require 'paperclip/matchers/validate_attachment_content_type_matcher'
|
||||||
|
require 'paperclip/matchers/validate_attachment_size_matcher'
|
||||||
49
lib/paperclip/matchers/have_attached_file_matcher.rb
Normal file
49
lib/paperclip/matchers/have_attached_file_matcher.rb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
module Paperclip
|
||||||
|
module Shoulda
|
||||||
|
module Matchers
|
||||||
|
def have_attached_file name
|
||||||
|
HaveAttachedFileMatcher.new(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
class HaveAttachedFileMatcher
|
||||||
|
def initialize attachment_name
|
||||||
|
@attachment_name = attachment_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches? subject
|
||||||
|
@subject = subject
|
||||||
|
responds? && has_column? && included?
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message
|
||||||
|
"Should have an attachment named #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def negative_failure_message
|
||||||
|
"Should not have an attachment named #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
"have an attachment named #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def responds?
|
||||||
|
methods = @subject.instance_methods
|
||||||
|
methods.include?("#{@attachment_name}") &&
|
||||||
|
methods.include?("#{@attachment_name}=") &&
|
||||||
|
methods.include?("#{@attachment_name}?")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_column?
|
||||||
|
@subject.column_names.include?("#{@attachment_name}_file_name")
|
||||||
|
end
|
||||||
|
|
||||||
|
def included?
|
||||||
|
@subject.ancestors.include?(Paperclip::InstanceMethods)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
module Paperclip
|
||||||
|
module Shoulda
|
||||||
|
module Matchers
|
||||||
|
def validate_attachment_content_type name
|
||||||
|
ValidateAttachmentContentTypeMatcher.new(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
class ValidateAttachmentContentTypeMatcher
|
||||||
|
def initialize attachment_name
|
||||||
|
@attachment_name = attachment_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowing *types
|
||||||
|
@allowed_types = types.flatten
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def rejecting *types
|
||||||
|
@rejected_types = types.flatten
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches? subject
|
||||||
|
@subject = subject
|
||||||
|
@allowed_types && @rejected_types &&
|
||||||
|
allowed_types_allowed? && rejected_types_rejected?
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message
|
||||||
|
"Content types #{@allowed_types.join(", ")} should be accepted" +
|
||||||
|
" and #{@rejected_types.join(", ")} rejected by #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def negative_failure_message
|
||||||
|
"Content types #{@allowed_types.join(", ")} should be rejected" +
|
||||||
|
" and #{@rejected_types.join(", ")} accepted by #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
"validate the content types allowed on attachment #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def allow_types?(types)
|
||||||
|
types.all? do |type|
|
||||||
|
file = StringIO.new(".")
|
||||||
|
file.content_type = type
|
||||||
|
attachment = @subject.new.attachment_for(@attachment_name)
|
||||||
|
attachment.assign(file)
|
||||||
|
attachment.errors[:content_type].nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_types_allowed?
|
||||||
|
allow_types?(@allowed_types)
|
||||||
|
end
|
||||||
|
|
||||||
|
def rejected_types_rejected?
|
||||||
|
not allow_types?(@rejected_types)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
module Paperclip
|
||||||
|
module Shoulda
|
||||||
|
module Matchers
|
||||||
|
def validate_attachment_presence name
|
||||||
|
ValidateAttachmentPresenceMatcher.new(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
class ValidateAttachmentPresenceMatcher
|
||||||
|
def initialize attachment_name
|
||||||
|
@attachment_name = attachment_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches? subject
|
||||||
|
@subject = subject
|
||||||
|
error_when_not_valid? && no_error_when_valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message
|
||||||
|
"Attachment #{@attachment_name} should be required"
|
||||||
|
end
|
||||||
|
|
||||||
|
def negative_failure_message
|
||||||
|
"Attachment #{@attachment_name} should not be required"
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
"require presence of attachment #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def error_when_not_valid?
|
||||||
|
@attachment = @subject.new.send(@attachment_name)
|
||||||
|
@attachment.assign(nil)
|
||||||
|
not @attachment.errors[:presence].nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_error_when_valid?
|
||||||
|
@file = StringIO.new(".")
|
||||||
|
@attachment = @subject.new.send(@attachment_name)
|
||||||
|
@attachment.assign(@file)
|
||||||
|
@attachment.errors[:presence].nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
83
lib/paperclip/matchers/validate_attachment_size_matcher.rb
Normal file
83
lib/paperclip/matchers/validate_attachment_size_matcher.rb
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
module Paperclip
|
||||||
|
module Shoulda
|
||||||
|
module Matchers
|
||||||
|
def validate_attachment_size name
|
||||||
|
ValidateAttachmentSizeMatcher.new(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
class ValidateAttachmentSizeMatcher
|
||||||
|
def initialize attachment_name
|
||||||
|
@attachment_name = attachment_name
|
||||||
|
@low, @high = 0, (1.0/0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def less_than size
|
||||||
|
@high = size
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def greater_than size
|
||||||
|
@low = size
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def in range
|
||||||
|
@low, @high = range.first, range.last
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches? subject
|
||||||
|
@subject = subject
|
||||||
|
lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message
|
||||||
|
"Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes"
|
||||||
|
end
|
||||||
|
|
||||||
|
def negative_failure_message
|
||||||
|
"Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes"
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
"validate the size of attachment #{@attachment_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def override_method object, method, &replacement
|
||||||
|
(class << object; self; end).class_eval do
|
||||||
|
define_method(method, &replacement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def passes_validation_with_size(new_size)
|
||||||
|
file = StringIO.new(".")
|
||||||
|
override_method(file, :size){ new_size }
|
||||||
|
attachment = @subject.new.attachment_for(@attachment_name)
|
||||||
|
attachment.assign(file)
|
||||||
|
attachment.errors[:size].nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def lower_than_low?
|
||||||
|
not passes_validation_with_size(@low - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def higher_than_low?
|
||||||
|
passes_validation_with_size(@low + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lower_than_high?
|
||||||
|
return true if @high == (1.0/0)
|
||||||
|
passes_validation_with_size(@high - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def higher_than_high?
|
||||||
|
return true if @high == (1.0/0)
|
||||||
|
not passes_validation_with_size(@high + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
44
lib/paperclip/optimizer.rb
Normal file
44
lib/paperclip/optimizer.rb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
require 'open3'
|
||||||
|
|
||||||
|
module Paperclip
|
||||||
|
class Optimizer < Processor
|
||||||
|
def make
|
||||||
|
optimized_file_path = optimize(@file)
|
||||||
|
if optimized_file_path && File.exists?(optimized_file_path)
|
||||||
|
return File.open(optimized_file_path)
|
||||||
|
else
|
||||||
|
return @file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def real_content_type
|
||||||
|
out = Paperclip.run "file", "--mime-type #{@file.path.shellescape}"
|
||||||
|
out.split(/:\s+/)[1].gsub("\n", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
def optimize(file)
|
||||||
|
src = @file.path
|
||||||
|
dst = "#{src}-#{SecureRandom.hex}"
|
||||||
|
src_shell = src.shellescape
|
||||||
|
dst_shell = dst.shellescape
|
||||||
|
cmd = case real_content_type
|
||||||
|
when 'image/jpeg', 'image/jpg', 'image/pjpeg'
|
||||||
|
"cp #{src_shell} #{dst_shell} && jpegoptim --all-progressive -q --strip-com --strip-exif --strip-iptc -- #{dst_shell}"
|
||||||
|
when 'image/png', 'image/x-png'
|
||||||
|
"pngcrush -rem alla -q #{src_shell} #{dst_shell}"
|
||||||
|
when 'image/gif'
|
||||||
|
"gifsicle -o #{dst_shell} -O3 --no-comments --no-names --same-delay --same-loopcount --no-warnings -- #{src_shell}"
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
run_and_verify!(cmd)
|
||||||
|
dst
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_and_verify!(cmd)
|
||||||
|
# Checking stdout and stderr because pngcrush always has exit code of zero
|
||||||
|
Open3.capture3(cmd)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
49
lib/paperclip/processor.rb
Normal file
49
lib/paperclip/processor.rb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
module Paperclip
|
||||||
|
# Paperclip processors allow you to modify attached files when they are
|
||||||
|
# attached in any way you are able. Paperclip itself uses command-line
|
||||||
|
# programs for its included Thumbnail processor, but custom processors
|
||||||
|
# are not required to follow suit.
|
||||||
|
#
|
||||||
|
# Processors are required to be defined inside the Paperclip module and
|
||||||
|
# are also required to be a subclass of Paperclip::Processor. There is
|
||||||
|
# only one method you *must* implement to properly be a subclass:
|
||||||
|
# #make, but #initialize may also be of use. Both methods accept 3
|
||||||
|
# arguments: the file that will be operated on (which is an instance of
|
||||||
|
# File), a hash of options that were defined in has_attached_file's
|
||||||
|
# style hash, and the Paperclip::Attachment itself.
|
||||||
|
#
|
||||||
|
# All #make needs to return is an instance of File (Tempfile is
|
||||||
|
# acceptable) which contains the results of the processing.
|
||||||
|
#
|
||||||
|
# See Paperclip.run for more information about using command-line
|
||||||
|
# utilities from within Processors.
|
||||||
|
class Processor
|
||||||
|
attr_accessor :file, :options, :attachment
|
||||||
|
|
||||||
|
def initialize file, options = {}, attachment = nil
|
||||||
|
@file = file
|
||||||
|
@options = options
|
||||||
|
@attachment = attachment
|
||||||
|
end
|
||||||
|
|
||||||
|
def make
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.make file, options = {}, attachment = nil
|
||||||
|
new(file, options, attachment).make
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Due to how ImageMagick handles its image format conversion and how Tempfile
|
||||||
|
# handles its naming scheme, it is necessary to override how Tempfile makes
|
||||||
|
# its names so as to allow for file extensions. Idea taken from the comments
|
||||||
|
# on this blog post:
|
||||||
|
# http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
|
||||||
|
class Tempfile < ::Tempfile
|
||||||
|
# Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
|
||||||
|
def make_tmpname(basename, n)
|
||||||
|
extension = File.extname(basename)
|
||||||
|
sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n.to_i, extension)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
lib/paperclip/recursive_thumbnail.rb
Normal file
12
lib/paperclip/recursive_thumbnail.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Paperclip
|
||||||
|
class RecursiveThumbnail < Thumbnail
|
||||||
|
def initialize file, options = {}, attachment = nil
|
||||||
|
|
||||||
|
# если по каким-то причинам не сформировался файл
|
||||||
|
# для прыдущего размера не кидаем ексепшен и
|
||||||
|
# генерим файл из оригинального
|
||||||
|
f = attachment.to_file(options[:thumbnail] || :original) rescue file
|
||||||
|
super f, options, attachment
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
346
lib/paperclip/storage.rb
Normal file
346
lib/paperclip/storage.rb
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
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 = default_style)
|
||||||
|
if original_filename
|
||||||
|
File.exist?(path(style))
|
||||||
|
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 = default_style
|
||||||
|
@queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
|
||||||
|
end
|
||||||
|
alias_method :to_io, :to_file
|
||||||
|
|
||||||
|
def flush_writes #:nodoc:
|
||||||
|
@queued_for_write.each do |style, file|
|
||||||
|
file.close
|
||||||
|
FileUtils.mkdir_p(File.dirname(path(style)))
|
||||||
|
log("saving #{path(style)}")
|
||||||
|
FileUtils.mv(file.path, path(style))
|
||||||
|
FileUtils.chmod(0644, path(style))
|
||||||
|
end
|
||||||
|
@queued_for_write = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def flush_deletes #:nodoc:
|
||||||
|
@queued_for_delete.each do |path|
|
||||||
|
begin
|
||||||
|
log("deleting #{path}")
|
||||||
|
FileUtils.rm(path)
|
||||||
|
rescue Errno::ENOENT => e
|
||||||
|
# ignore file-not-found, let everything else pass
|
||||||
|
end
|
||||||
|
begin
|
||||||
|
while(true)
|
||||||
|
path = File.dirname(path)
|
||||||
|
if Dir.entries(path).empty?
|
||||||
|
FileUtils.rmdir(path)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
# Need to create boolean field synced_to_s3
|
||||||
|
module Delayeds3
|
||||||
|
# require 'aws-sdk'
|
||||||
|
module ClassMethods
|
||||||
|
def parse_credentials creds
|
||||||
|
return @parsed_credentials if @parsed_credentials
|
||||||
|
creds = find_credentials(creds).stringify_keys
|
||||||
|
@parsed_credentials ||= (creds[Rails.env] || creds).symbolize_keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_credentials creds
|
||||||
|
case creds
|
||||||
|
when File
|
||||||
|
YAML.load_file(creds.path)
|
||||||
|
when String
|
||||||
|
YAML.load_file(creds)
|
||||||
|
when Hash
|
||||||
|
creds
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Credentials are not a path, file, or hash."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class WriteToS3Job < Struct.new(:class_name, :name, :id)
|
||||||
|
def perform
|
||||||
|
WriteToS3Worker.new.perform(class_name, name, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class UploadWorker
|
||||||
|
include ::Sidekiq::Worker
|
||||||
|
sidekiq_options queue: ::IMAGE_UPLOAD_QUEUE
|
||||||
|
|
||||||
|
def perform(class_name, name, id)
|
||||||
|
file = class_name.constantize.find_by_id(id)
|
||||||
|
return unless file
|
||||||
|
attachment = file.send(name)
|
||||||
|
write(attachment)
|
||||||
|
attachment.delete_local_files!
|
||||||
|
rescue Errno::ESTALE
|
||||||
|
raise if attachment && file.class.exists?(file)
|
||||||
|
rescue Errno::ENOENT => e
|
||||||
|
raise if attachment && file.class.exists?(file)
|
||||||
|
Rollbar.warn(e, file_name: extract_file_name_from_error(e))
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_file_name_from_error(err)
|
||||||
|
err.message.split(' - ')[-1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class WriteToS3Worker < UploadWorker
|
||||||
|
def write(attachment)
|
||||||
|
attachment.write_to_s3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class WriteToFogWorker < UploadWorker
|
||||||
|
def write(attachment)
|
||||||
|
attachment.write_to_fog
|
||||||
|
rescue Excon::Errors::SocketError => e
|
||||||
|
raise e.socket_error if e.socket_error.is_a?(Errno::ENOENT) || e.socket_error.is_a?(Errno::ESTALE)
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_storage
|
||||||
|
@s3_credentials = self.class.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]
|
||||||
|
@job_priority = @options[:job_priority]
|
||||||
|
|
||||||
|
@fog_provider = @options[:fog_provider]
|
||||||
|
@fog_directory = @options[:fog_directory]
|
||||||
|
@fog_credentials = @options[:fog_credentials]
|
||||||
|
|
||||||
|
@s3_url = ":s3_path_url" unless @s3_url.to_s.match(/^:s3.*url$/)
|
||||||
|
Paperclip.interpolates(:s3_alias_url) do |attachment, style|
|
||||||
|
":cdn_protocol://:cdn_domain/#{attachment.path(style).gsub(%r{^/}, "")}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def aws_bucket
|
||||||
|
return @aws_bucket if @aws_bucket
|
||||||
|
|
||||||
|
s3_client = Aws::S3::Client.new(
|
||||||
|
region: @s3_credentials[:region] || 'us-east-1',
|
||||||
|
access_key_id: @s3_credentials[:access_key_id],
|
||||||
|
secret_access_key: @s3_credentials[:secret_access_key]
|
||||||
|
)
|
||||||
|
|
||||||
|
s3_resource = Aws::S3::Resource.new(client: s3_client)
|
||||||
|
@aws_bucket = s3_resource.bucket(bucket_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def bucket_name
|
||||||
|
@bucket
|
||||||
|
end
|
||||||
|
|
||||||
|
def s3_host_alias
|
||||||
|
@s3_host_alias
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_file style = default_style
|
||||||
|
@queued_for_write[style] || (File.new(filesystem_path(style), 'rb') if exists?(style)) || download_file(style)
|
||||||
|
end
|
||||||
|
|
||||||
|
def download_file(style = default_style)
|
||||||
|
return unless instance_read(:synced_to_s3)
|
||||||
|
temp_file = Tempfile.new(['product_image', File.extname(filesystem_path(style))], encoding: 'ascii-8bit')
|
||||||
|
uri = URI(URI.encode(url(style)))
|
||||||
|
Net::HTTP.start(uri.host, uri.port) do |http|
|
||||||
|
req = Net::HTTP::Get.new uri
|
||||||
|
http.request(req) do |response|
|
||||||
|
if response.is_a?(Net::HTTPOK)
|
||||||
|
response.read_body{|chunk| temp_file.write(chunk)}
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
temp_file.flush
|
||||||
|
temp_file.rewind
|
||||||
|
temp_file
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :to_io, :to_file
|
||||||
|
|
||||||
|
def parse_credentials creds
|
||||||
|
creds = find_credentials(creds).stringify_keys
|
||||||
|
(creds[Rails.env] || creds).symbolize_keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def s3_protocol
|
||||||
|
@s3_protocol
|
||||||
|
end
|
||||||
|
|
||||||
|
def exists?(style = default_style)
|
||||||
|
File.exist?(filesystem_path(style))
|
||||||
|
end
|
||||||
|
|
||||||
|
def s3_path style
|
||||||
|
interpolate(options[:s3_path], style)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filesystem_paths
|
||||||
|
h = {}
|
||||||
|
[:original, *@styles.keys].uniq.map do |style|
|
||||||
|
h[style] = filesystem_path(style) if File.exists?(filesystem_path(style))
|
||||||
|
end
|
||||||
|
h
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_to_s3
|
||||||
|
return true if instance_read(:synced_to_s3)
|
||||||
|
filesystem_paths.each do |style, file|
|
||||||
|
log("saving to s3 #{file}")
|
||||||
|
s3_object = aws_bucket.object(s3_path(style).gsub(/^\/+/,''))
|
||||||
|
s3_object.upload_file(file,
|
||||||
|
cache_control: "max-age=#{10.year.to_i}",
|
||||||
|
content_type: instance_read(:content_type),
|
||||||
|
expires: 10.year.from_now.httpdate,
|
||||||
|
acl: 'public-read')
|
||||||
|
end
|
||||||
|
instance.send("#{name}_synced_to_s3=", true)
|
||||||
|
instance.save
|
||||||
|
end
|
||||||
|
|
||||||
|
def fog_storage
|
||||||
|
@fog_storage ||= Fog::Storage.new(@fog_credentials.merge(provider: @fog_provider).symbolize_keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_to_fog
|
||||||
|
return unless instance.respond_to? "#{name}_synced_to_fog"
|
||||||
|
return true if instance_read(:synced_to_fog)
|
||||||
|
paths = filesystem_paths
|
||||||
|
raise RuntimeError.new("Local files not found for Image:#{instance_read(:id)}") if paths.length < styles.length # To make monitoring easier
|
||||||
|
paths.each do |style, file|
|
||||||
|
path = s3_path(style)
|
||||||
|
path = path[1..-1] if path.start_with?('/')
|
||||||
|
log "Saving to Fog with key #{path}"
|
||||||
|
options = {
|
||||||
|
"Content-Type" => instance_read(:content_type),
|
||||||
|
"Cache-Control" => "max-age=#{10.year.to_i}",
|
||||||
|
"x-goog-acl" => "public-read"
|
||||||
|
}
|
||||||
|
|
||||||
|
File.open(file, 'r') do |f|
|
||||||
|
fog_storage.put_object @fog_directory, path, f, options
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# не вызываем колбеки и спокойно себя ведем если объект удален
|
||||||
|
instance.send("#{name}_synced_to_fog=", true)
|
||||||
|
instance.save
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def flush_writes #:nodoc:
|
||||||
|
@queued_for_write.each do |style, file|
|
||||||
|
file.close
|
||||||
|
FileUtils.mkdir_p(File.dirname(filesystem_path(style)))
|
||||||
|
log("saving to filesystem #{filesystem_path(style)}")
|
||||||
|
FileUtils.mv(file.path, filesystem_path(style))
|
||||||
|
FileUtils.chmod(0644, filesystem_path(style))
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @queued_for_write.empty? || (delay_processing? && @was_dirty)
|
||||||
|
instance.update_column("#{name}_synced_to_s3", false) if instance_read(:synced_to_s3)
|
||||||
|
@queued_jobs.push -> {
|
||||||
|
WriteToS3Worker.perform_async(instance.class.to_s, @name, instance.id)
|
||||||
|
WriteToFogWorker.perform_async(instance.class.to_s, @name, instance.id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
@queued_for_write = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a file and all parent directories if they are empty
|
||||||
|
def delete_recursive(path)
|
||||||
|
initial_path = path
|
||||||
|
begin
|
||||||
|
FileUtils.rm(path) if File.exist?(path)
|
||||||
|
rescue Errno::ENOENT, Errno::ESTALE => e
|
||||||
|
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::ESTALE => e
|
||||||
|
rescue SystemCallError => e
|
||||||
|
Rollbar.error(e, {path: path, initial_path: initial_path})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def flush_deletes #:nodoc:
|
||||||
|
# если мы картинку заливали в облака, значит мы скорее всего ее уже удалили
|
||||||
|
# и можно не нагружать хранилище проверками
|
||||||
|
if !instance.is_a?(AccountFile) && instance_read(:synced_to_fog) &&
|
||||||
|
instance_read(:synced_to_s3)
|
||||||
|
@queued_for_delete = []
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@queued_for_delete.each do |path|
|
||||||
|
log("Deleting local file #{path}")
|
||||||
|
delete_recursive(path)
|
||||||
|
end
|
||||||
|
@queued_for_delete = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_local_files!
|
||||||
|
return if instance.is_a?(AccountFile)
|
||||||
|
instance.reload
|
||||||
|
if instance_read(:synced_to_fog) && instance_read(:synced_to_s3)
|
||||||
|
filesystem_paths.values.each do |filename|
|
||||||
|
log("Deleting local file #{filename}")
|
||||||
|
delete_recursive(filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
91
lib/paperclip/thumbnail.rb
Normal file
91
lib/paperclip/thumbnail.rb
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
module Paperclip
|
||||||
|
# Handles thumbnailing images that are uploaded.
|
||||||
|
class Thumbnail < Processor
|
||||||
|
|
||||||
|
attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options
|
||||||
|
|
||||||
|
# Creates a Thumbnail object set to work on the +file+ given. It
|
||||||
|
# will attempt to transform the image into one defined by +target_geometry+
|
||||||
|
# which is a "WxH"-style string. +format+ will be inferred from the +file+
|
||||||
|
# unless specified. Thumbnail creation will raise no errors unless
|
||||||
|
# +whiny+ is true (which it is, by default. If +convert_options+ is
|
||||||
|
# set, the options will be appended to the convert command upon image conversion
|
||||||
|
def initialize file, options = {}, attachment = nil
|
||||||
|
super
|
||||||
|
geometry = options[:geometry]
|
||||||
|
@file = file
|
||||||
|
@crop = geometry[-1,1] == '#'
|
||||||
|
@target_geometry = Geometry.parse geometry
|
||||||
|
@current_geometry = Geometry.from_file @file
|
||||||
|
@convert_options = options[:convert_options]
|
||||||
|
@whiny = options[:whiny].nil? ? true : options[:whiny]
|
||||||
|
@format = options[:format]
|
||||||
|
|
||||||
|
@current_format = File.extname(@file.path)
|
||||||
|
@basename = File.basename(@file.path, @current_format)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the +target_geometry+ is meant to crop.
|
||||||
|
def crop?
|
||||||
|
@crop
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the image is meant to make use of additional convert options.
|
||||||
|
def convert_options?
|
||||||
|
not @convert_options.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
|
||||||
|
# that contains the new image.
|
||||||
|
def make
|
||||||
|
src = @file
|
||||||
|
dst = Tempfile.new([@basename, @format].compact.join("."))
|
||||||
|
dst.binmode
|
||||||
|
|
||||||
|
command = <<-end_command
|
||||||
|
"#{ File.expand_path(src.path) }[0]"
|
||||||
|
#{ transformation_command }
|
||||||
|
#{ gamma_correction_if_needed }
|
||||||
|
"#{ File.expand_path(dst.path) }"
|
||||||
|
end_command
|
||||||
|
|
||||||
|
begin
|
||||||
|
success = Paperclip.run("convert", command.gsub(/\s+/, " "))
|
||||||
|
rescue PaperclipCommandLineError
|
||||||
|
raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
|
||||||
|
end
|
||||||
|
|
||||||
|
dst
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the command ImageMagick's +convert+ needs to transform the image
|
||||||
|
# into the thumbnail.
|
||||||
|
def transformation_command
|
||||||
|
scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
|
||||||
|
trans = "-resize \"#{scale}\""
|
||||||
|
trans << " -crop \"#{crop}\" +repage" if crop
|
||||||
|
trans << " #{convert_options}" if convert_options?
|
||||||
|
trans
|
||||||
|
end
|
||||||
|
|
||||||
|
def gamma_correction_if_needed
|
||||||
|
command = <<-end_command
|
||||||
|
-format "%[magick]\\n%[type]"
|
||||||
|
"#{ File.expand_path(@file.path) }[0]"
|
||||||
|
end_command
|
||||||
|
|
||||||
|
begin
|
||||||
|
result = Paperclip.run("identify", command.gsub(/\s+/, ' '))
|
||||||
|
rescue PaperclipCommandLineError
|
||||||
|
raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
|
||||||
|
end
|
||||||
|
|
||||||
|
magick, type = result.split("\n")
|
||||||
|
if magick == 'PNG'
|
||||||
|
'-define png:big-depth=16 -define png:color-type=6'
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
64
lib/paperclip/upfile.rb
Normal file
64
lib/paperclip/upfile.rb
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
module Paperclip
|
||||||
|
# The Upfile module is a convenience module for adding uploaded-file-type methods
|
||||||
|
# to the +File+ class. Useful for testing.
|
||||||
|
# user.avatar = File.new("test/test_avatar.jpg")
|
||||||
|
module Upfile
|
||||||
|
# Infer the MIME-type of the file from the extension.
|
||||||
|
def content_type
|
||||||
|
Paperclip::Upfile.content_type self.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.content_type path
|
||||||
|
type = (path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
|
||||||
|
case type
|
||||||
|
when %r"jpe?g" then "image/jpeg"
|
||||||
|
when %r"tiff?" then "image/tiff"
|
||||||
|
when %r"png", "gif", "bmp" then "image/#{type}"
|
||||||
|
when "txt" then "text/plain"
|
||||||
|
when %r"html?" then "text/html"
|
||||||
|
when "csv", "xml", "css", "js" then "text/#{type}"
|
||||||
|
when "liquid" then "text/x-liquid"
|
||||||
|
else "application/x-#{type}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the file's normal name.
|
||||||
|
def original_filename
|
||||||
|
File.basename(self.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the size of the file.
|
||||||
|
def size
|
||||||
|
File.size(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if defined? StringIO
|
||||||
|
class StringIO
|
||||||
|
attr_accessor :original_filename, :content_type
|
||||||
|
|
||||||
|
def original_filename
|
||||||
|
@original_filename ||= "stringio.txt"
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_type
|
||||||
|
@content_type ||= Paperclip::Upfile.content_type original_filename
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FastUploadFile < File
|
||||||
|
attr_accessor :original_filename, :content_type
|
||||||
|
|
||||||
|
def initialize(uploaded_file)
|
||||||
|
@original_filename = uploaded_file['original_name']
|
||||||
|
@content_type = uploaded_file['content_type']
|
||||||
|
super uploaded_file['filepath']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class File #:nodoc:
|
||||||
|
include Paperclip::Upfile
|
||||||
|
end
|
||||||
|
|
||||||
79
lib/tasks/paperclip_tasks.rake
Normal file
79
lib/tasks/paperclip_tasks.rake
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
def obtain_class
|
||||||
|
class_name = ENV['CLASS'] || ENV['class']
|
||||||
|
raise "Must specify CLASS" unless class_name
|
||||||
|
@klass = Object.const_get(class_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def obtain_attachments
|
||||||
|
name = ENV['ATTACHMENT'] || ENV['attachment']
|
||||||
|
raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions)
|
||||||
|
if !name.blank? && @klass.attachment_definitions.keys.include?(name)
|
||||||
|
[ name ]
|
||||||
|
else
|
||||||
|
@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
|
||||||
|
print result ? "." : "x"; $stdout.flush
|
||||||
|
end
|
||||||
|
end
|
||||||
|
puts " Done."
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :paperclip do
|
||||||
|
desc "Refreshes both metadata and thumbnails."
|
||||||
|
task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
|
||||||
|
|
||||||
|
namespace :refresh do
|
||||||
|
desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
|
||||||
|
task :thumbnails => :environment do
|
||||||
|
errors = []
|
||||||
|
for_all_attachments do |instance, name|
|
||||||
|
result = instance.send(name).reprocess!
|
||||||
|
errors << [instance.id, instance.errors] unless instance.errors.blank?
|
||||||
|
result
|
||||||
|
end
|
||||||
|
errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
|
||||||
|
task :metadata => :environment do
|
||||||
|
for_all_attachments do |instance, name|
|
||||||
|
if file = instance.send(name).to_file
|
||||||
|
instance.send("#{name}_file_name=", instance.send("#{name}_file_name").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.save(:validate => false)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Cleans out invalid attachments. Useful after you've added new validations."
|
||||||
|
task :clean => :environment do
|
||||||
|
for_all_attachments do |instance, name|
|
||||||
|
instance.send(name).send(:validate)
|
||||||
|
if instance.send(name).valid?
|
||||||
|
true
|
||||||
|
else
|
||||||
|
instance.send("#{name}=", nil)
|
||||||
|
instance.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
36
paperclip.gemspec
Normal file
36
paperclip.gemspec
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = %q{paperclip}
|
||||||
|
s.version = "2.2.9.2"
|
||||||
|
|
||||||
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
||||||
|
s.authors = ["Jon Yurek"]
|
||||||
|
s.date = %q{2009-06-18}
|
||||||
|
s.email = %q{jyurek@thoughtbot.com}
|
||||||
|
s.extra_rdoc_files = ["README.rdoc"]
|
||||||
|
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", "shoulda_macros/paperclip.rb"]
|
||||||
|
s.has_rdoc = true
|
||||||
|
s.homepage = %q{http://www.thoughtbot.com/projects/paperclip}
|
||||||
|
s.rdoc_options = ["--line-numbers", "--inline-source"]
|
||||||
|
s.require_paths = ["lib"]
|
||||||
|
s.requirements = ["ImageMagick"]
|
||||||
|
s.rubyforge_project = %q{paperclip}
|
||||||
|
s.rubygems_version = %q{1.3.1}
|
||||||
|
s.summary = %q{File attachments as attributes for ActiveRecord}
|
||||||
|
|
||||||
|
if s.respond_to? :specification_version then
|
||||||
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
||||||
|
s.specification_version = 2
|
||||||
|
|
||||||
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
||||||
|
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
||||||
|
s.add_development_dependency(%q<mocha>, [">= 0"])
|
||||||
|
else
|
||||||
|
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
||||||
|
s.add_dependency(%q<mocha>, [">= 0"])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
||||||
|
s.add_dependency(%q<mocha>, [">= 0"])
|
||||||
|
end
|
||||||
|
end
|
||||||
68
shoulda_macros/paperclip.rb
Normal file
68
shoulda_macros/paperclip.rb
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
require 'paperclip/matchers'
|
||||||
|
|
||||||
|
module Paperclip
|
||||||
|
# =Paperclip Shoulda Macros
|
||||||
|
#
|
||||||
|
# These macros are intended for use with shoulda, and will be included into
|
||||||
|
# your tests automatically. All of the macros use the standard shoulda
|
||||||
|
# assumption that the name of the test is based on the name of the model
|
||||||
|
# you're testing (that is, UserTest is the test for the User model), and
|
||||||
|
# will load that class for testing purposes.
|
||||||
|
module Shoulda
|
||||||
|
include Matchers
|
||||||
|
# This will test whether you have defined your attachment correctly by
|
||||||
|
# checking for all the required fields exist after the definition of the
|
||||||
|
# attachment.
|
||||||
|
def should_have_attached_file name
|
||||||
|
klass = self.name.gsub(/Test$/, '').constantize
|
||||||
|
matcher = have_attached_file name
|
||||||
|
should matcher.description do
|
||||||
|
assert_accepts(matcher, klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tests for validations on the presence of the attachment.
|
||||||
|
def should_validate_attachment_presence name
|
||||||
|
klass = self.name.gsub(/Test$/, '').constantize
|
||||||
|
matcher = validate_attachment_presence name
|
||||||
|
should matcher.description do
|
||||||
|
assert_accepts(matcher, klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tests that you have content_type validations specified. There are two
|
||||||
|
# options, :valid and :invalid. Both accept an array of strings. The
|
||||||
|
# strings should be a list of content types which will pass and fail
|
||||||
|
# validation, respectively.
|
||||||
|
def should_validate_attachment_content_type name, options = {}
|
||||||
|
klass = self.name.gsub(/Test$/, '').constantize
|
||||||
|
valid = [options[:valid]].flatten
|
||||||
|
invalid = [options[:invalid]].flatten
|
||||||
|
matcher = validate_attachment_content_type(name).allowing(valid).rejecting(invalid)
|
||||||
|
should matcher.description do
|
||||||
|
assert_accepts(matcher, klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tests to ensure that you have file size validations turned on. You
|
||||||
|
# can pass the same options to this that you can to
|
||||||
|
# validate_attachment_file_size - :less_than, :greater_than, and :in.
|
||||||
|
# :less_than checks that a file is less than a certain size, :greater_than
|
||||||
|
# checks that a file is more than a certain size, and :in takes a Range or
|
||||||
|
# Array which specifies the lower and upper limits of the file size.
|
||||||
|
def should_validate_attachment_size name, options = {}
|
||||||
|
klass = self.name.gsub(/Test$/, '').constantize
|
||||||
|
min = options[:greater_than] || (options[:in] && options[:in].first) || 0
|
||||||
|
max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
|
||||||
|
range = (min..max)
|
||||||
|
matcher = validate_attachment_size(name).in(range)
|
||||||
|
should matcher.description do
|
||||||
|
assert_accepts(matcher, klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Test::Unit::TestCase #:nodoc:
|
||||||
|
extend Paperclip::Shoulda
|
||||||
|
end
|
||||||
1
test/.gitignore
vendored
Normal file
1
test/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
debug.log
|
||||||
768
test/attachment_test.rb
Normal file
768
test/attachment_test.rb
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class Dummy
|
||||||
|
# This is a dummy class
|
||||||
|
end
|
||||||
|
|
||||||
|
class AttachmentTest < Test::Unit::TestCase
|
||||||
|
should "return the path based on the url by default" do
|
||||||
|
@attachment = attachment :url => "/:class/:id/:basename"
|
||||||
|
@model = @attachment.instance
|
||||||
|
@model.id = 1234
|
||||||
|
@model.avatar_file_name = "fake.jpg"
|
||||||
|
assert_equal "#{Rails.root}/public/fake_models/1234/fake", @attachment.path
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Attachment default_options" do
|
||||||
|
setup do
|
||||||
|
rebuild_model
|
||||||
|
@old_default_options = Paperclip::Attachment.default_options.dup
|
||||||
|
@new_default_options = @old_default_options.merge({
|
||||||
|
:path => "argle/bargle",
|
||||||
|
:url => "fooferon",
|
||||||
|
:default_url => "not here.png"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
Paperclip::Attachment.default_options.merge! @old_default_options
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be overrideable" do
|
||||||
|
Paperclip::Attachment.default_options.merge!(@new_default_options)
|
||||||
|
@new_default_options.keys.each do |key|
|
||||||
|
assert_equal @new_default_options[key],
|
||||||
|
Paperclip::Attachment.default_options[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without an Attachment" do
|
||||||
|
setup do
|
||||||
|
@dummy = Dummy.new
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return false when asked exists?" do
|
||||||
|
assert !@dummy.avatar.exists?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "on an Attachment" do
|
||||||
|
setup do
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@attachment = @dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
Paperclip::Attachment.default_options.keys.each do |key|
|
||||||
|
should "be the default_options for #{key}" do
|
||||||
|
assert_equal @old_default_options[key],
|
||||||
|
@attachment.instance_variable_get("@#{key}"),
|
||||||
|
key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when redefined" do
|
||||||
|
setup do
|
||||||
|
Paperclip::Attachment.default_options.merge!(@new_default_options)
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@attachment = @dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
Paperclip::Attachment.default_options.keys.each do |key|
|
||||||
|
should "be the new default_options for #{key}" do
|
||||||
|
assert_equal @new_default_options[key],
|
||||||
|
@attachment.instance_variable_get("@#{key}"),
|
||||||
|
key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with similarly named interpolations" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :path => ":id.omg/:id-bbq/:idwhat/:id_partition.wtf"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.stubs(:id).returns(1024)
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "make sure that they are interpolated correctly" do
|
||||||
|
assert_equal "1024.omg/1024-bbq/1024what/000/001/024.wtf", @dummy.avatar.path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with a :rails_env interpolation" do
|
||||||
|
setup do
|
||||||
|
@rails_env = "blah"
|
||||||
|
@id = 1024
|
||||||
|
rebuild_model :path => ":rails_env/:id.png"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.stubs(:id).returns(@id)
|
||||||
|
@file = StringIO.new(".")
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the proper path" do
|
||||||
|
temporary_rails_env(@rails_env) {
|
||||||
|
assert_equal "#{@rails_env}/#{@id}.png", @dummy.avatar.path
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with a default style and an extension interpolation" do
|
||||||
|
setup do
|
||||||
|
@attachment = attachment :path => ":basename.:extension",
|
||||||
|
:styles => { :default => ["100x100", :png] },
|
||||||
|
:default_style => :default
|
||||||
|
@file = StringIO.new("...")
|
||||||
|
@file.expects(:original_filename).returns("file.jpg")
|
||||||
|
end
|
||||||
|
should "return the right extension for the path" do
|
||||||
|
@attachment.assign(@file)
|
||||||
|
assert_equal "file.png", @attachment.path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with :convert_options" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => {
|
||||||
|
:thumb => "100x100",
|
||||||
|
:large => "400x400"
|
||||||
|
},
|
||||||
|
:convert_options => {
|
||||||
|
:all => "-do_stuff",
|
||||||
|
:thumb => "-thumbnailize"
|
||||||
|
}
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report the correct options when sent #extra_options_for(:thumb)" do
|
||||||
|
assert_equal "-thumbnailize -do_stuff", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report the correct options when sent #extra_options_for(:large)" do
|
||||||
|
assert_equal "-do_stuff", @dummy.avatar.send(:extra_options_for, :large)
|
||||||
|
end
|
||||||
|
|
||||||
|
before_should "call extra_options_for(:thumb/:large)" do
|
||||||
|
Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:thumb)
|
||||||
|
Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:large)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with :convert_options that is a proc" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => {
|
||||||
|
:thumb => "100x100",
|
||||||
|
:large => "400x400"
|
||||||
|
},
|
||||||
|
:convert_options => {
|
||||||
|
:all => lambda{|i| i.all },
|
||||||
|
:thumb => lambda{|i| i.thumb }
|
||||||
|
}
|
||||||
|
Dummy.class_eval do
|
||||||
|
def all; "-all"; end
|
||||||
|
def thumb; "-thumb"; end
|
||||||
|
end
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report the correct options when sent #extra_options_for(:thumb)" do
|
||||||
|
assert_equal "-thumb -all", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report the correct options when sent #extra_options_for(:large)" do
|
||||||
|
assert_equal "-all", @dummy.avatar.send(:extra_options_for, :large)
|
||||||
|
end
|
||||||
|
|
||||||
|
before_should "call extra_options_for(:thumb/:large)" do
|
||||||
|
Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:thumb)
|
||||||
|
Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:large)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with :path that is a proc" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :path => lambda{ |attachment| "path/#{attachment.instance.other}.:extension" }
|
||||||
|
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
@dummyA = Dummy.new(:other => 'a')
|
||||||
|
@dummyA.avatar = @file
|
||||||
|
@dummyB = Dummy.new(:other => 'b')
|
||||||
|
@dummyB.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "return correct path" do
|
||||||
|
assert_equal "path/a.png", @dummyA.avatar.path
|
||||||
|
assert_equal "path/b.png", @dummyB.avatar.path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with :styles that is a proc" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => lambda{ |attachment| {:thumb => "50x50#", :large => "400x400"} }
|
||||||
|
|
||||||
|
@attachment = Dummy.new.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have the correct geometry" do
|
||||||
|
assert_equal "50x50#", @attachment.styles[:thumb][:geometry]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with :url that is a proc" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :url => lambda{ |attachment| "path/#{attachment.instance.other}.:extension" }
|
||||||
|
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
@dummyA = Dummy.new(:other => 'a')
|
||||||
|
@dummyA.avatar = @file
|
||||||
|
@dummyB = Dummy.new(:other => 'b')
|
||||||
|
@dummyB.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "return correct url" do
|
||||||
|
assert_equal "path/a.png", @dummyA.avatar.url(:original, false)
|
||||||
|
assert_equal "path/b.png", @dummyB.avatar.url(:original, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
geometry_specs = [
|
||||||
|
[ lambda{|z| "50x50#" }, :png ],
|
||||||
|
lambda{|z| "50x50#" },
|
||||||
|
{ :geometry => lambda{|z| "50x50#" } }
|
||||||
|
]
|
||||||
|
geometry_specs.each do |geometry_spec|
|
||||||
|
context "An attachment geometry like #{geometry_spec}" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :normal => geometry_spec }
|
||||||
|
@attachment = Dummy.new.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not run the procs immediately" do
|
||||||
|
assert_kind_of Proc, @attachment.styles[:normal][:geometry]
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when assigned" do
|
||||||
|
setup do
|
||||||
|
@file = StringIO.new(".")
|
||||||
|
@attachment.assign(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have the correct geometry" do
|
||||||
|
assert_equal "50x50#", @attachment.styles[:normal][:geometry]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with both 'normal' and hash-style styles" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => {
|
||||||
|
:normal => ["50x50#", :png],
|
||||||
|
:hash => { :geometry => "50x50#", :format => :png }
|
||||||
|
}
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@attachment = @dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
[:processors, :whiny, :convert_options, :geometry, :format].each do |field|
|
||||||
|
should "have the same #{field} field" do
|
||||||
|
assert_equal @attachment.styles[:normal][field], @attachment.styles[:hash][field]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with :processors that is a proc" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :normal => '' }, :processors => lambda { |a| [ :test ] }
|
||||||
|
@attachment = Dummy.new.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not run the proc immediately" do
|
||||||
|
assert_kind_of Proc, @attachment.styles[:normal][:processors]
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when assigned" do
|
||||||
|
setup do
|
||||||
|
@attachment.assign(StringIO.new("."))
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have the correct processors" do
|
||||||
|
assert_equal [ :test ], @attachment.styles[:normal][:processors]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with erroring processor" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :processor => [:thumbnail], :styles => { :small => '' }, :whiny_thumbnails => true
|
||||||
|
@dummy = Dummy.new
|
||||||
|
Paperclip::Thumbnail.expects(:make).raises(Paperclip::PaperclipError, "cannot be processed.")
|
||||||
|
@file = StringIO.new("...")
|
||||||
|
@file.stubs(:to_tempfile).returns(@file)
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "correctly forward processing error message to the instance" do
|
||||||
|
@dummy.valid?
|
||||||
|
assert_contains @dummy.errors.full_messages, "Avatar cannot be processed."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with multiple processors" do
|
||||||
|
setup do
|
||||||
|
class Paperclip::Test < Paperclip::Processor; end
|
||||||
|
@style_params = { :once => {:one => 1, :two => 2} }
|
||||||
|
rebuild_model :processors => [:thumbnail, :test], :styles => @style_params
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = StringIO.new("...")
|
||||||
|
@file.stubs(:to_tempfile).returns(@file)
|
||||||
|
Paperclip::Test.stubs(:make).returns(@file)
|
||||||
|
Paperclip::Thumbnail.stubs(:make).returns(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when assigned" do
|
||||||
|
setup { @dummy.avatar = @file }
|
||||||
|
|
||||||
|
before_should "call #make on all specified processors" do
|
||||||
|
expected_params = @style_params[:once].merge({:processors => [:thumbnail, :test], :whiny => true, :convert_options => ""})
|
||||||
|
Paperclip::Thumbnail.expects(:make).with(@file, expected_params, @dummy.avatar).returns(@file)
|
||||||
|
Paperclip::Test.expects(:make).with(@file, expected_params, @dummy.avatar).returns(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
before_should "call #make with attachment passed as third argument" do
|
||||||
|
expected_params = @style_params[:once].merge({:processors => [:thumbnail, :test], :whiny => true, :convert_options => ""})
|
||||||
|
Paperclip::Test.expects(:make).with(@file, expected_params, @dummy.avatar).returns(@file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with no processors defined" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :processors => [], :styles => {:something => 1}
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = StringIO.new("...")
|
||||||
|
end
|
||||||
|
should "raise when assigned to" do
|
||||||
|
assert_raises(RuntimeError){ @dummy.avatar = @file }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Assigning an attachment with post_process hooks" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :something => "100x100#" }
|
||||||
|
Dummy.class_eval do
|
||||||
|
before_avatar_post_process :do_before_avatar
|
||||||
|
after_avatar_post_process :do_after_avatar
|
||||||
|
before_post_process :do_before_all
|
||||||
|
after_post_process :do_after_all
|
||||||
|
def do_before_avatar; end
|
||||||
|
def do_after_avatar; end
|
||||||
|
def do_before_all; end
|
||||||
|
def do_after_all; end
|
||||||
|
end
|
||||||
|
@file = StringIO.new(".")
|
||||||
|
@file.stubs(:to_tempfile).returns(@file)
|
||||||
|
@dummy = Dummy.new
|
||||||
|
Paperclip::Thumbnail.stubs(:make).returns(@file)
|
||||||
|
@attachment = @dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "call the defined callbacks when assigned" do
|
||||||
|
@dummy.expects(:do_before_avatar).with()
|
||||||
|
@dummy.expects(:do_after_avatar).with()
|
||||||
|
@dummy.expects(:do_before_all).with()
|
||||||
|
@dummy.expects(:do_after_all).with()
|
||||||
|
Paperclip::Thumbnail.expects(:make).returns(@file)
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not cancel the processing if a before_post_process returns nil" do
|
||||||
|
@dummy.expects(:do_before_avatar).with().returns(nil)
|
||||||
|
@dummy.expects(:do_after_avatar).with()
|
||||||
|
@dummy.expects(:do_before_all).with().returns(nil)
|
||||||
|
@dummy.expects(:do_after_all).with()
|
||||||
|
Paperclip::Thumbnail.expects(:make).returns(@file)
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "cancel the processing if a before_post_process returns false" do
|
||||||
|
@dummy.expects(:do_before_avatar).never
|
||||||
|
@dummy.expects(:do_after_avatar).never
|
||||||
|
@dummy.expects(:do_before_all).with().returns(false)
|
||||||
|
@dummy.expects(:do_after_all).never
|
||||||
|
Paperclip::Thumbnail.expects(:make).never
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "cancel the processing if a before_avatar_post_process returns false" do
|
||||||
|
@dummy.expects(:do_before_avatar).with().returns(false)
|
||||||
|
@dummy.expects(:do_after_avatar).never
|
||||||
|
@dummy.expects(:do_before_all).with().returns(true)
|
||||||
|
@dummy.expects(:do_after_all).never
|
||||||
|
Paperclip::Thumbnail.expects(:make).never
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Assigning an attachment" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :something => "100x100#" }
|
||||||
|
@file = StringIO.new(".")
|
||||||
|
@file.expects(:original_filename).returns("5k.png\n\n")
|
||||||
|
@file.expects(:content_type).returns("image/png\n\n")
|
||||||
|
@file.stubs(:to_tempfile).returns(@file)
|
||||||
|
@dummy = Dummy.new
|
||||||
|
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
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "strip whitespace from original_filename field" do
|
||||||
|
assert_equal "5k.png", @dummy.avatar.original_filename
|
||||||
|
end
|
||||||
|
|
||||||
|
should "strip whitespace from content_type field" do
|
||||||
|
assert_equal "image/png", @dummy.avatar.instance.avatar_content_type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Attachment with strange letters" do
|
||||||
|
setup do
|
||||||
|
rebuild_model
|
||||||
|
|
||||||
|
@not_file = mock
|
||||||
|
@tempfile = mock
|
||||||
|
@not_file.stubs(:nil?).returns(false)
|
||||||
|
@not_file.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(:content_type).returns("image/png\r\n")
|
||||||
|
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@attachment = @dummy.avatar
|
||||||
|
@attachment.expects(:valid_assignment?).with(@not_file).returns(true)
|
||||||
|
@attachment.expects(:queue_existing_for_delete)
|
||||||
|
@attachment.expects(:post_process)
|
||||||
|
@attachment.expects(:valid?).returns(true)
|
||||||
|
@attachment.expects(:validate)
|
||||||
|
@dummy.avatar = @not_file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "remove strange letters and replace with underscore (_)" do
|
||||||
|
assert_equal "sheep_say_b_.png", @dummy.avatar.original_filename
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment" 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
|
||||||
|
@attachment = Paperclip::Attachment.new(:avatar, @instance)
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@file.close
|
||||||
|
Paperclip::Attachment.default_options.merge!(@old_defaults)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "raise if there are not the correct columns when you try to assign" do
|
||||||
|
@other_attachment = Paperclip::Attachment.new(:not_here, @instance)
|
||||||
|
assert_raises(Paperclip::PaperclipError) do
|
||||||
|
@other_attachment.assign(@file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return its default_url when no file assigned" do
|
||||||
|
assert @attachment.to_file.nil?
|
||||||
|
assert_equal "/avatars/original/missing.png", @attachment.url
|
||||||
|
assert_equal "/avatars/blah/missing.png", @attachment.url(:blah)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return nil as path when no file assigned" do
|
||||||
|
assert @attachment.to_file.nil?
|
||||||
|
assert_equal nil, @attachment.path
|
||||||
|
assert_equal nil, @attachment.path(:blah)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a file assigned in the database" do
|
||||||
|
setup do
|
||||||
|
@attachment.stubs(:instance_read).with(:file_name).returns("5k.png")
|
||||||
|
@attachment.stubs(:instance_read).with(:content_type).returns("image/png")
|
||||||
|
@attachment.stubs(:instance_read).with(:file_size).returns(12345)
|
||||||
|
now = Time.now
|
||||||
|
Time.stubs(:now).returns(now)
|
||||||
|
@attachment.stubs(:instance_read).with(:updated_at).returns(Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return a correct url even if the file does not exist" do
|
||||||
|
assert_nil @attachment.to_file
|
||||||
|
assert_match %r{^/system/avatars/#{@instance.id}/blah/5k\.png}, @attachment.url(:blah)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "make sure the updated_at mtime is in the url if it is defined" do
|
||||||
|
assert_match %r{#{Time.now.to_i}$}, @attachment.url(:blah)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "make sure the updated_at mtime is NOT in the url if false is passed to the url method" do
|
||||||
|
assert_no_match %r{#{Time.now.to_i}$}, @attachment.url(:blah, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with the updated_at field removed" do
|
||||||
|
setup do
|
||||||
|
@attachment.stubs(:instance_read).with(:updated_at).returns(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "only return the url without the updated_at when sent #url" do
|
||||||
|
assert_match "/avatars/#{@instance.id}/blah/5k.png", @attachment.url(:blah)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the proper path when filename has a single .'s" do
|
||||||
|
assert_equal "./test/../tmp/avatars/dummies/original/#{@instance.id}/5k.png", @attachment.path
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the proper path when filename has multiple .'s" do
|
||||||
|
@attachment.stubs(:instance_read).with(:file_name).returns("5k.old.png")
|
||||||
|
assert_equal "./test/../tmp/avatars/dummies/original/#{@instance.id}/5k.old.png", @attachment.path
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when expecting three styles" do
|
||||||
|
setup do
|
||||||
|
styles = {:styles => { :large => ["400x400", :png],
|
||||||
|
:medium => ["100x100", :gif],
|
||||||
|
:small => ["32x32#", :jpg]}}
|
||||||
|
@attachment = Paperclip::Attachment.new(:avatar,
|
||||||
|
@instance,
|
||||||
|
styles)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and assigned a file" do
|
||||||
|
setup do
|
||||||
|
now = Time.now
|
||||||
|
Time.stubs(:now).returns(now)
|
||||||
|
@attachment.assign(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be dirty" do
|
||||||
|
assert @attachment.dirty?
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and saved" do
|
||||||
|
setup do
|
||||||
|
@attachment.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the real url" do
|
||||||
|
file = @attachment.to_file
|
||||||
|
assert file
|
||||||
|
assert_match %r{^/system/avatars/#{@instance.id}/original/5k\.png}, @attachment.url
|
||||||
|
assert_match %r{^/system/avatars/#{@instance.id}/small/5k\.jpg}, @attachment.url(:small)
|
||||||
|
file.close
|
||||||
|
end
|
||||||
|
|
||||||
|
should "commit the files to disk" do
|
||||||
|
[:large, :medium, :small].each do |style|
|
||||||
|
io = @attachment.to_io(style)
|
||||||
|
assert File.exists?(io)
|
||||||
|
assert ! io.is_a?(::Tempfile)
|
||||||
|
io.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "save the files as the right formats and sizes" do
|
||||||
|
[[:large, 400, 61, "PNG"],
|
||||||
|
[:medium, 100, 15, "GIF"],
|
||||||
|
[:small, 32, 32, "JPEG"]].each do |style|
|
||||||
|
cmd = %Q[identify -format "%w %h %b %m" "#{@attachment.path(style.first)}"]
|
||||||
|
out = `#{cmd}`
|
||||||
|
width, height, size, format = out.split(" ")
|
||||||
|
assert_equal style[1].to_s, width.to_s
|
||||||
|
assert_equal style[2].to_s, height.to_s
|
||||||
|
assert_equal style[3].to_s, format.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "still have its #file attribute not be nil" do
|
||||||
|
assert ! (file = @attachment.to_file).nil?
|
||||||
|
file.close
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and trying to delete" do
|
||||||
|
setup do
|
||||||
|
@existing_names = @attachment.styles.keys.collect do |style|
|
||||||
|
@attachment.path(style)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "delete the files after assigning nil" do
|
||||||
|
@attachment.expects(:instance_write).with(:file_name, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:content_type, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:file_size, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:updated_at, nil)
|
||||||
|
@attachment.assign nil
|
||||||
|
@attachment.save
|
||||||
|
@existing_names.each{|f| assert ! File.exists?(f) }
|
||||||
|
end
|
||||||
|
|
||||||
|
should "delete the files when you call #clear and #save" do
|
||||||
|
@attachment.expects(:instance_write).with(:file_name, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:content_type, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:file_size, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:updated_at, nil)
|
||||||
|
@attachment.clear
|
||||||
|
@attachment.save
|
||||||
|
@existing_names.each{|f| assert ! File.exists?(f) }
|
||||||
|
end
|
||||||
|
|
||||||
|
should "delete the files when you call #delete" do
|
||||||
|
@attachment.expects(:instance_write).with(:file_name, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:content_type, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:file_size, nil)
|
||||||
|
@attachment.expects(:instance_write).with(:updated_at, nil)
|
||||||
|
@attachment.destroy
|
||||||
|
@existing_names.each{|f| assert ! File.exists?(f) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when trying a nonexistant storage type" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :not_here
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not be able to find the module" do
|
||||||
|
assert_raise(NameError){ Dummy.new.avatar }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with only a avatar_file_name column" do
|
||||||
|
setup do
|
||||||
|
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
||||||
|
table.column :avatar_file_name, :string
|
||||||
|
end
|
||||||
|
rebuild_class
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "not error when assigned an attachment" do
|
||||||
|
assert_nothing_raised { @dummy.avatar = @file }
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the time when sent #avatar_updated_at" do
|
||||||
|
now = Time.now
|
||||||
|
Time.stubs(:now).returns(now)
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert now, @dummy.avatar.updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return nil when reloaded and sent #avatar_updated_at" do
|
||||||
|
@dummy.save
|
||||||
|
@dummy.reload
|
||||||
|
assert_nil @dummy.avatar.updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the right value when sent #avatar_file_size" do
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert_equal @file.size, @dummy.avatar.size
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and avatar_updated_at column" do
|
||||||
|
setup do
|
||||||
|
ActiveRecord::Base.connection.add_column :dummies, :avatar_updated_at, :timestamp
|
||||||
|
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_updated_at" do
|
||||||
|
now = Time.now
|
||||||
|
Time.stubs(:now).returns(now)
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert_equal now.to_i, @dummy.avatar.updated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and avatar_content_type column" do
|
||||||
|
setup do
|
||||||
|
ActiveRecord::Base.connection.add_column :dummies, :avatar_content_type, :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_content_type" do
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert_equal "image/png", @dummy.avatar.content_type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and avatar_file_size column" do
|
||||||
|
setup do
|
||||||
|
ActiveRecord::Base.connection.add_column :dummies, :avatar_file_size, :integer
|
||||||
|
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_file_size" do
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert_equal @file.size, @dummy.avatar.size
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the right value when saved, reloaded, and sent #avatar_file_size" do
|
||||||
|
@dummy.avatar = @file
|
||||||
|
@dummy.save
|
||||||
|
@dummy = Dummy.find(@dummy.id)
|
||||||
|
assert_equal @file.size, @dummy.avatar.size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
4
test/database.yml
Normal file
4
test/database.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
test:
|
||||||
|
adapter: sqlite3
|
||||||
|
database: ":memory:"
|
||||||
|
|
||||||
BIN
test/fixtures/12k.png
vendored
Normal file
BIN
test/fixtures/12k.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
test/fixtures/50x50.png
vendored
Normal file
BIN
test/fixtures/50x50.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
test/fixtures/5k.png
vendored
Normal file
BIN
test/fixtures/5k.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
1
test/fixtures/bad.png
vendored
Normal file
1
test/fixtures/bad.png
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is not an image.
|
||||||
4
test/fixtures/s3.yml
vendored
Normal file
4
test/fixtures/s3.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
development:
|
||||||
|
key: 54321
|
||||||
|
production:
|
||||||
|
key: 12345
|
||||||
0
test/fixtures/text.txt
vendored
Normal file
0
test/fixtures/text.txt
vendored
Normal file
BIN
test/fixtures/twopage.pdf
vendored
Normal file
BIN
test/fixtures/twopage.pdf
vendored
Normal file
Binary file not shown.
177
test/geometry_test.rb
Normal file
177
test/geometry_test.rb
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class GeometryTest < Test::Unit::TestCase
|
||||||
|
context "Paperclip::Geometry" do
|
||||||
|
should "correctly report its given dimensions" do
|
||||||
|
assert @geo = Paperclip::Geometry.new(1024, 768)
|
||||||
|
assert_equal 1024, @geo.width
|
||||||
|
assert_equal 768, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "set height to 0 if height dimension is missing" do
|
||||||
|
assert @geo = Paperclip::Geometry.new(1024)
|
||||||
|
assert_equal 1024, @geo.width
|
||||||
|
assert_equal 0, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "set width to 0 if width dimension is missing" do
|
||||||
|
assert @geo = Paperclip::Geometry.new(nil, 768)
|
||||||
|
assert_equal 0, @geo.width
|
||||||
|
assert_equal 768, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be generated from a WxH-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("800x600")
|
||||||
|
assert_equal 800, @geo.width
|
||||||
|
assert_equal 600, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be generated from a xH-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("x600")
|
||||||
|
assert_equal 0, @geo.width
|
||||||
|
assert_equal 600, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be generated from a Wx-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("800x")
|
||||||
|
assert_equal 800, @geo.width
|
||||||
|
assert_equal 0, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be generated from a W-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("800")
|
||||||
|
assert_equal 800, @geo.width
|
||||||
|
assert_equal 0, @geo.height
|
||||||
|
end
|
||||||
|
|
||||||
|
should "ensure the modifier is nil if not present" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("123x456")
|
||||||
|
assert_nil @geo.modifier
|
||||||
|
end
|
||||||
|
|
||||||
|
should "treat x and X the same in geometries" do
|
||||||
|
@lower = Paperclip::Geometry.parse("123x456")
|
||||||
|
@upper = Paperclip::Geometry.parse("123X456")
|
||||||
|
assert_equal 123, @lower.width
|
||||||
|
assert_equal 123, @upper.width
|
||||||
|
assert_equal 456, @lower.height
|
||||||
|
assert_equal 456, @upper.height
|
||||||
|
end
|
||||||
|
|
||||||
|
['>', '<', '#', '@', '%', '^', '!', nil].each do |mod|
|
||||||
|
should "ensure the modifier #{mod.inspect} is preserved" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("123x456#{mod}")
|
||||||
|
assert_equal mod, @geo.modifier
|
||||||
|
assert_equal "123x456#{mod}", @geo.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
['>', '<', '#', '@', '%', '^', '!', nil].each do |mod|
|
||||||
|
should "ensure the modifier #{mod.inspect} is preserved with no height" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("123x#{mod}")
|
||||||
|
assert_equal mod, @geo.modifier
|
||||||
|
assert_equal "123#{mod}", @geo.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "make sure the modifier gets passed during transformation_to" do
|
||||||
|
assert @src = Paperclip::Geometry.parse("123x456")
|
||||||
|
assert @dst = Paperclip::Geometry.parse("123x456>")
|
||||||
|
assert_equal "123x456>", @src.transformation_to(@dst).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "generate correct ImageMagick formatting string for W-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("800")
|
||||||
|
assert_equal "800", @geo.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "generate correct ImageMagick formatting string for Wx-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("800x")
|
||||||
|
assert_equal "800", @geo.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "generate correct ImageMagick formatting string for xH-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("x600")
|
||||||
|
assert_equal "x600", @geo.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "generate correct ImageMagick formatting string for WxH-formatted string" do
|
||||||
|
assert @geo = Paperclip::Geometry.parse("800x600")
|
||||||
|
assert_equal "800x600", @geo.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be generated from a file" do
|
||||||
|
file = File.join(File.dirname(__FILE__), "fixtures", "5k.png")
|
||||||
|
file = File.new(file, 'rb')
|
||||||
|
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
|
||||||
|
assert @geo.height > 0
|
||||||
|
assert @geo.width > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be generated from a file path" do
|
||||||
|
file = File.join(File.dirname(__FILE__), "fixtures", "5k.png")
|
||||||
|
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
|
||||||
|
assert @geo.height > 0
|
||||||
|
assert @geo.width > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not generate from a bad file" do
|
||||||
|
file = "/home/This File Does Not Exist.omg"
|
||||||
|
assert_raise(Paperclip::NotIdentifiedByImageMagickError){ @geo = Paperclip::Geometry.from_file(file) }
|
||||||
|
end
|
||||||
|
|
||||||
|
[['vertical', 900, 1440, true, false, false, 1440, 900, 0.625],
|
||||||
|
['horizontal', 1024, 768, false, true, false, 1024, 768, 1.3333],
|
||||||
|
['square', 100, 100, false, false, true, 100, 100, 1]].each do |args|
|
||||||
|
context "performing calculations on a #{args[0]} viewport" do
|
||||||
|
setup do
|
||||||
|
@geo = Paperclip::Geometry.new(args[1], args[2])
|
||||||
|
end
|
||||||
|
|
||||||
|
should "#{args[3] ? "" : "not"} be vertical" do
|
||||||
|
assert_equal args[3], @geo.vertical?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "#{args[4] ? "" : "not"} be horizontal" do
|
||||||
|
assert_equal args[4], @geo.horizontal?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "#{args[5] ? "" : "not"} be square" do
|
||||||
|
assert_equal args[5], @geo.square?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report that #{args[6]} is the larger dimension" do
|
||||||
|
assert_equal args[6], @geo.larger
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report that #{args[7]} is the smaller dimension" do
|
||||||
|
assert_equal args[7], @geo.smaller
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have an aspect ratio of #{args[8]}" do
|
||||||
|
assert_in_delta args[8], @geo.aspect, 0.0001
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[[ [1000, 100], [64, 64], "x64", "64x64+288+0" ],
|
||||||
|
[ [100, 1000], [50, 950], "x950", "50x950+22+0" ],
|
||||||
|
[ [100, 1000], [50, 25], "50x", "50x25+0+237" ]]. each do |args|
|
||||||
|
context "of #{args[0].inspect} and given a Geometry #{args[1].inspect} and sent transform_to" do
|
||||||
|
setup do
|
||||||
|
@geo = Paperclip::Geometry.new(*args[0])
|
||||||
|
@dst = Paperclip::Geometry.new(*args[1])
|
||||||
|
@scale, @crop = @geo.transformation_to @dst, true
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to return the correct scaling transformation geometry #{args[2]}" do
|
||||||
|
assert_equal args[2], @scale
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to return the correct crop transformation geometry #{args[3]}" do
|
||||||
|
assert_equal args[3], @crop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
100
test/helper.rb
Normal file
100
test/helper.rb
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
require 'rubygems'
|
||||||
|
require 'test/unit'
|
||||||
|
gem 'thoughtbot-shoulda', ">= 2.9.0"
|
||||||
|
require 'shoulda'
|
||||||
|
require 'mocha'
|
||||||
|
require 'tempfile'
|
||||||
|
|
||||||
|
gem 'sqlite3-ruby'
|
||||||
|
|
||||||
|
require 'active_record'
|
||||||
|
require 'active_support'
|
||||||
|
begin
|
||||||
|
require 'ruby-debug'
|
||||||
|
rescue LoadError
|
||||||
|
puts "ruby-debug not loaded"
|
||||||
|
end
|
||||||
|
|
||||||
|
ROOT = File.join(File.dirname(__FILE__), '..')
|
||||||
|
Rails.root = ROOT
|
||||||
|
Rails.env = "test"
|
||||||
|
|
||||||
|
$LOAD_PATH << File.join(ROOT, 'lib')
|
||||||
|
$LOAD_PATH << File.join(ROOT, 'lib', 'paperclip')
|
||||||
|
|
||||||
|
require File.join(ROOT, 'lib', 'paperclip.rb')
|
||||||
|
|
||||||
|
require 'shoulda_macros/paperclip'
|
||||||
|
|
||||||
|
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
|
||||||
|
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
||||||
|
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
||||||
|
ActiveRecord::Base.establish_connection(config['test'])
|
||||||
|
|
||||||
|
def reset_class class_name
|
||||||
|
ActiveRecord::Base.send(:include, Paperclip)
|
||||||
|
Object.send(:remove_const, class_name) rescue nil
|
||||||
|
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
|
||||||
|
klass.class_eval{ include Paperclip }
|
||||||
|
klass
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_table table_name, &block
|
||||||
|
block ||= lambda{ true }
|
||||||
|
ActiveRecord::Base.connection.create_table :dummies, {:force => true}, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def modify_table table_name, &block
|
||||||
|
ActiveRecord::Base.connection.change_table :dummies, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_model options = {}
|
||||||
|
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
|
||||||
|
table.column :other, :string
|
||||||
|
table.column :avatar_file_name, :string
|
||||||
|
table.column :avatar_content_type, :string
|
||||||
|
table.column :avatar_file_size, :integer
|
||||||
|
table.column :avatar_updated_at, :datetime
|
||||||
|
end
|
||||||
|
rebuild_class options
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_class options = {}
|
||||||
|
ActiveRecord::Base.send(:include, Paperclip)
|
||||||
|
Object.send(:remove_const, "Dummy") rescue nil
|
||||||
|
Object.const_set("Dummy", Class.new(ActiveRecord::Base))
|
||||||
|
Dummy.class_eval do
|
||||||
|
include Paperclip
|
||||||
|
has_attached_file :avatar, options
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class FakeModel
|
||||||
|
attr_accessor :avatar_file_name,
|
||||||
|
:avatar_file_size,
|
||||||
|
:avatar_last_updated,
|
||||||
|
:avatar_content_type,
|
||||||
|
:id
|
||||||
|
|
||||||
|
def errors
|
||||||
|
@errors ||= []
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_callbacks name, *args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachment options
|
||||||
|
Paperclip::Attachment.new(:avatar, FakeModel.new, options)
|
||||||
|
end
|
||||||
481
test/integration_test.rb
Normal file
481
test/integration_test.rb
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class IntegrationTest < Test::Unit::TestCase
|
||||||
|
context "Many models at once" do
|
||||||
|
setup do
|
||||||
|
rebuild_model
|
||||||
|
@file = File.new(File.join(FIXTURES_DIR, "5k.png"), 'rb')
|
||||||
|
300.times do |i|
|
||||||
|
Dummy.create! :avatar => @file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not exceed the open file limit" do
|
||||||
|
assert_nothing_raised do
|
||||||
|
dummies = Dummy.find(:all)
|
||||||
|
dummies.each { |dummy| dummy.avatar }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :thumb => "50x50#" }
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert @dummy.save
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "create its thumbnails properly" do
|
||||||
|
assert_match /\b50x50\b/, `identify "#{@dummy.avatar.path(:thumb)}"`
|
||||||
|
end
|
||||||
|
|
||||||
|
context "redefining its attachment styles" do
|
||||||
|
setup do
|
||||||
|
Dummy.class_eval do
|
||||||
|
has_attached_file :avatar, :styles => { :thumb => "150x25#" }
|
||||||
|
has_attached_file :avatar, :styles => { :thumb => "150x25#", :dynamic => lambda { |a| '50x50#' } }
|
||||||
|
end
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
@d2.avatar.reprocess!
|
||||||
|
@d2.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create its thumbnails properly" do
|
||||||
|
assert_match /\b150x25\b/, `identify "#{@dummy.avatar.path(:thumb)}"`
|
||||||
|
assert_match /\b50x50\b/, `identify "#{@dummy.avatar.path(:dynamic)}"`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model that modifies its original" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :original => "2x2#" }
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report the file size of the processed file and not the original" do
|
||||||
|
assert_not_equal @file.size, @dummy.avatar.size
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model with attachments scoped under an id" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :large => "100x100",
|
||||||
|
:medium => "50x50" },
|
||||||
|
:path => ":rails_root/tmp/:id/:attachments/:style.:extension"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
context "when saved" do
|
||||||
|
setup do
|
||||||
|
@dummy.save
|
||||||
|
@saved_path = @dummy.avatar.path(:large)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have a large file in the right place" do
|
||||||
|
assert File.exists?(@dummy.avatar.path(:large))
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and deleted" do
|
||||||
|
setup do
|
||||||
|
@dummy.avatar.clear
|
||||||
|
@dummy.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not have a large file in the right place anymore" do
|
||||||
|
assert ! File.exists?(@saved_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not have its next two parent directories" do
|
||||||
|
assert ! File.exists?(File.dirname(@saved_path))
|
||||||
|
assert ! File.exists?(File.dirname(File.dirname(@saved_path)))
|
||||||
|
end
|
||||||
|
|
||||||
|
before_should "not die if an unexpected SystemCallError happens" do
|
||||||
|
FileUtils.stubs(:rmdir).raises(Errno::EPIPE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model with no attachment validation" 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 false when asked about whiny_thumbnails" do
|
||||||
|
assert ! Dummy.attachment_definitions[:avatar][:whiny_thumbnails]
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when validates_attachment_thumbnails is called" do
|
||||||
|
setup do
|
||||||
|
Dummy.validates_attachment_thumbnails :avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have its definition return true when asked about whiny_thumbnails" do
|
||||||
|
assert_equal true, Dummy.attachment_definitions[:avatar][:whiny_thumbnails]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "redefined to have attachment validations" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :large => "300x300>",
|
||||||
|
:medium => "100x100",
|
||||||
|
:thumb => ["32x32#", :gif] },
|
||||||
|
:whiny_thumbnails => true,
|
||||||
|
: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 true when asked about whiny_thumbnails" do
|
||||||
|
assert_equal true, Dummy.attachment_definitions[:avatar][:whiny_thumbnails]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model with no convert_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 convert_options" do
|
||||||
|
assert ! Dummy.attachment_definitions[:avatar][:convert_options]
|
||||||
|
end
|
||||||
|
|
||||||
|
context "redefined to have convert_options setting" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :large => "300x300>",
|
||||||
|
:medium => "100x100",
|
||||||
|
:thumb => ["32x32#", :gif] },
|
||||||
|
:convert_options => "-strip -depth 8",
|
||||||
|
: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 convert_options value when asked about convert_options" do
|
||||||
|
assert_equal "-strip -depth 8", Dummy.attachment_definitions[:avatar][:convert_options]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model with a filesystem attachment" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :large => "300x300>",
|
||||||
|
:medium => "100x100",
|
||||||
|
:thumb => ["32x32#", :gif] },
|
||||||
|
:whiny_thumbnails => true,
|
||||||
|
:default_style => :medium,
|
||||||
|
:url => "/:attachment/:class/:style/:id/:basename.:extension",
|
||||||
|
:path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = File.new(File.join(FIXTURES_DIR, "5k.png"), 'rb')
|
||||||
|
@bad_file = File.new(File.join(FIXTURES_DIR, "bad.png"), 'rb')
|
||||||
|
|
||||||
|
assert @dummy.avatar = @file
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert @dummy.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "write and delete its files" do
|
||||||
|
[["434x66", :original],
|
||||||
|
["300x46", :large],
|
||||||
|
["100x15", :medium],
|
||||||
|
["32x32", :thumb]].each do |geo, style|
|
||||||
|
cmd = %Q[identify -format "%wx%h" "#{@dummy.avatar.path(style)}"]
|
||||||
|
assert_equal geo, `#{cmd}`.chomp, cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.path(s) }
|
||||||
|
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
assert_equal "100x15", `identify -format "%wx%h" "#{@d2.avatar.path}"`.chomp
|
||||||
|
assert_equal "434x66", `identify -format "%wx%h" "#{@d2.avatar.path(:original)}"`.chomp
|
||||||
|
assert_equal "300x46", `identify -format "%wx%h" "#{@d2.avatar.path(:large)}"`.chomp
|
||||||
|
assert_equal "100x15", `identify -format "%wx%h" "#{@d2.avatar.path(:medium)}"`.chomp
|
||||||
|
assert_equal "32x32", `identify -format "%wx%h" "#{@d2.avatar.path(:thumb)}"`.chomp
|
||||||
|
|
||||||
|
@dummy.avatar = "not a valid file but not nil"
|
||||||
|
assert_equal File.basename(@file.path), @dummy.avatar_file_name
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert @dummy.save
|
||||||
|
|
||||||
|
saved_paths.each do |p|
|
||||||
|
assert File.exists?(p)
|
||||||
|
end
|
||||||
|
|
||||||
|
@dummy.avatar.clear
|
||||||
|
assert_nil @dummy.avatar_file_name
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert @dummy.save
|
||||||
|
|
||||||
|
saved_paths.each do |p|
|
||||||
|
assert ! File.exists?(p)
|
||||||
|
end
|
||||||
|
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
assert_nil @d2.avatar_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
should "work exactly the same when new as when reloaded" do
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
|
||||||
|
assert_equal @dummy.avatar_file_name, @d2.avatar_file_name
|
||||||
|
[:thumb, :medium, :large, :original].each do |style|
|
||||||
|
assert_equal @dummy.avatar.path(style), @d2.avatar.path(style)
|
||||||
|
end
|
||||||
|
|
||||||
|
saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.path(s) }
|
||||||
|
|
||||||
|
@d2.avatar.clear
|
||||||
|
assert @d2.save
|
||||||
|
|
||||||
|
saved_paths.each do |p|
|
||||||
|
assert ! File.exists?(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "know the difference between good files, bad files, and not files" do
|
||||||
|
expected = @dummy.avatar.to_file
|
||||||
|
@dummy.avatar = "not a file"
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert_equal expected.path, @dummy.avatar.path
|
||||||
|
expected.close
|
||||||
|
|
||||||
|
@dummy.avatar = @bad_file
|
||||||
|
assert ! @dummy.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "know the difference between good files, bad files, and not files when validating" do
|
||||||
|
Dummy.validates_attachment_presence :avatar
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
@d2.avatar = @file
|
||||||
|
assert @d2.valid?, @d2.errors.full_messages.inspect
|
||||||
|
@d2.avatar = @bad_file
|
||||||
|
assert ! @d2.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to reload without saving and not have the file disappear" do
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert @dummy.save
|
||||||
|
@dummy.avatar.clear
|
||||||
|
assert_nil @dummy.avatar_file_name
|
||||||
|
@dummy.reload
|
||||||
|
assert_equal "5k.png", @dummy.avatar_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
context "that is assigned its file from another Paperclip attachment" do
|
||||||
|
setup do
|
||||||
|
@dummy2 = Dummy.new
|
||||||
|
@file2 = File.new(File.join(FIXTURES_DIR, "12k.png"), 'rb')
|
||||||
|
assert @dummy2.avatar = @file2
|
||||||
|
@dummy2.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "work when assigned a file" do
|
||||||
|
assert_not_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`,
|
||||||
|
`identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"`
|
||||||
|
|
||||||
|
assert @dummy.avatar = @dummy2.avatar
|
||||||
|
@dummy.save
|
||||||
|
assert_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`,
|
||||||
|
`identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model with an attachments association and a Paperclip attachment" do
|
||||||
|
setup do
|
||||||
|
Dummy.class_eval do
|
||||||
|
has_many :attachments, :class_name => 'Dummy'
|
||||||
|
end
|
||||||
|
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.avatar = File.new(File.join(File.dirname(__FILE__),
|
||||||
|
"fixtures",
|
||||||
|
"5k.png"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
should "should not error when saving" do
|
||||||
|
assert_nothing_raised do
|
||||||
|
@dummy.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if ENV['S3_TEST_BUCKET']
|
||||||
|
def s3_files_for attachment
|
||||||
|
[:thumb, :medium, :large, :original].inject({}) do |files, style|
|
||||||
|
data = `curl "#{attachment.url(style)}" 2>/dev/null`.chomp
|
||||||
|
t = Tempfile.new("paperclip-test")
|
||||||
|
t.binmode
|
||||||
|
t.write(data)
|
||||||
|
t.rewind
|
||||||
|
files[style] = t
|
||||||
|
files
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def s3_headers_for attachment, style
|
||||||
|
`curl --head "#{attachment.url(style)}" 2>/dev/null`.split("\n").inject({}) do |h,head|
|
||||||
|
split_head = head.chomp.split(/\s*:\s*/, 2)
|
||||||
|
h[split_head.first.downcase] = split_head.last unless split_head.empty?
|
||||||
|
h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A model with an S3 attachment" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :large => "300x300>",
|
||||||
|
:medium => "100x100",
|
||||||
|
:thumb => ["32x32#", :gif] },
|
||||||
|
:storage => :s3,
|
||||||
|
:whiny_thumbnails => true,
|
||||||
|
# :s3_options => {:logger => Logger.new(StringIO.new)},
|
||||||
|
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "s3.yml")),
|
||||||
|
:default_style => :medium,
|
||||||
|
:bucket => ENV['S3_TEST_BUCKET'],
|
||||||
|
:path => ":class/:attachment/:id/:style/:basename.:extension"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@file = File.new(File.join(FIXTURES_DIR, "5k.png"), 'rb')
|
||||||
|
@bad_file = File.new(File.join(FIXTURES_DIR, "bad.png"), 'rb')
|
||||||
|
|
||||||
|
assert @dummy.avatar = @file
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert @dummy.save
|
||||||
|
|
||||||
|
@files_on_s3 = s3_files_for @dummy.avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
should "write and delete its files" do
|
||||||
|
[["434x66", :original],
|
||||||
|
["300x46", :large],
|
||||||
|
["100x15", :medium],
|
||||||
|
["32x32", :thumb]].each do |geo, style|
|
||||||
|
cmd = %Q[identify -format "%wx%h" "#{@files_on_s3[style].path}"]
|
||||||
|
assert_equal geo, `#{cmd}`.chomp, cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
@d2_files = s3_files_for @d2.avatar
|
||||||
|
[["434x66", :original],
|
||||||
|
["300x46", :large],
|
||||||
|
["100x15", :medium],
|
||||||
|
["32x32", :thumb]].each do |geo, style|
|
||||||
|
cmd = %Q[identify -format "%wx%h" "#{@d2_files[style].path}"]
|
||||||
|
assert_equal geo, `#{cmd}`.chomp, cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
@dummy.avatar = "not a valid file but not nil"
|
||||||
|
assert_equal File.basename(@file.path), @dummy.avatar_file_name
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert @dummy.save
|
||||||
|
|
||||||
|
saved_keys = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.to_file(s) }
|
||||||
|
|
||||||
|
saved_keys.each do |key|
|
||||||
|
assert key.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
@dummy.avatar.clear
|
||||||
|
assert_nil @dummy.avatar_file_name
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert @dummy.save
|
||||||
|
|
||||||
|
saved_keys.each do |key|
|
||||||
|
assert ! key.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
assert_nil @d2.avatar_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
should "work exactly the same when new as when reloaded" do
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
|
||||||
|
assert_equal @dummy.avatar_file_name, @d2.avatar_file_name
|
||||||
|
[:thumb, :medium, :large, :original].each do |style|
|
||||||
|
assert_equal @dummy.avatar.to_file(style).to_s, @d2.avatar.to_file(style).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
saved_keys = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.to_file(s) }
|
||||||
|
|
||||||
|
@d2.avatar.clear
|
||||||
|
assert @d2.save
|
||||||
|
|
||||||
|
saved_keys.each do |key|
|
||||||
|
assert ! key.exists?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "know the difference between good files, bad files, not files, and nil" do
|
||||||
|
expected = @dummy.avatar.to_file
|
||||||
|
@dummy.avatar = "not a file"
|
||||||
|
assert @dummy.valid?
|
||||||
|
assert_equal expected.full_name, @dummy.avatar.to_file.full_name
|
||||||
|
|
||||||
|
@dummy.avatar = @bad_file
|
||||||
|
assert ! @dummy.valid?
|
||||||
|
@dummy.avatar = nil
|
||||||
|
assert @dummy.valid?
|
||||||
|
|
||||||
|
Dummy.validates_attachment_presence :avatar
|
||||||
|
@d2 = Dummy.find(@dummy.id)
|
||||||
|
@d2.avatar = @file
|
||||||
|
assert @d2.valid?
|
||||||
|
@d2.avatar = @bad_file
|
||||||
|
assert ! @d2.valid?
|
||||||
|
@d2.avatar = nil
|
||||||
|
assert ! @d2.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to reload without saving and not have the file disappear" do
|
||||||
|
@dummy.avatar = @file
|
||||||
|
assert @dummy.save
|
||||||
|
@dummy.avatar = nil
|
||||||
|
assert_nil @dummy.avatar_file_name
|
||||||
|
@dummy.reload
|
||||||
|
assert_equal "5k.png", @dummy.avatar_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have the right content type" do
|
||||||
|
headers = s3_headers_for(@dummy.avatar, :original)
|
||||||
|
p headers
|
||||||
|
assert_equal 'image/png', headers['content-type']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
120
test/interpolations_test.rb
Normal file
120
test/interpolations_test.rb
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class InterpolationsTest < Test::Unit::TestCase
|
||||||
|
should "return all methods but the infrastructure when sent #all" do
|
||||||
|
methods = Paperclip::Interpolations.all
|
||||||
|
assert ! methods.include?(:[])
|
||||||
|
assert ! methods.include?(:[]=)
|
||||||
|
assert ! methods.include?(:all)
|
||||||
|
methods.each do |m|
|
||||||
|
assert Paperclip::Interpolations.respond_to? m
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the Rails.root" do
|
||||||
|
assert_equal Rails.root, Paperclip::Interpolations.rails_root(:attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the Rails.env" do
|
||||||
|
assert_equal Rails.env, Paperclip::Interpolations.rails_env(:attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the class of the instance" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:instance).returns(attachment)
|
||||||
|
attachment.expects(:class).returns("Thing")
|
||||||
|
assert_equal "things", Paperclip::Interpolations.class(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the basename of the file" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:original_filename).returns("one.jpg").times(2)
|
||||||
|
assert_equal "one", Paperclip::Interpolations.basename(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the extension of the file" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:original_filename).returns("one.jpg")
|
||||||
|
attachment.expects(:styles).returns({})
|
||||||
|
assert_equal "jpg", Paperclip::Interpolations.extension(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the extension of the file as the format if defined in the style" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:original_filename).never
|
||||||
|
attachment.expects(:styles).returns({:style => {:format => "png"}})
|
||||||
|
assert_equal "png", Paperclip::Interpolations.extension(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the id of the attachment" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:id).returns(23)
|
||||||
|
attachment.expects(:instance).returns(attachment)
|
||||||
|
assert_equal 23, Paperclip::Interpolations.id(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the partitioned id of the attachment" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:id).returns(23)
|
||||||
|
attachment.expects(:instance).returns(attachment)
|
||||||
|
assert_equal "000/000/023", Paperclip::Interpolations.id_partition(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the name of the attachment" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:name).returns("file")
|
||||||
|
assert_equal "files", Paperclip::Interpolations.attachment(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the style" do
|
||||||
|
assert_equal :style, Paperclip::Interpolations.style(:attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the default style" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:default_style).returns(:default_style)
|
||||||
|
assert_equal :default_style, Paperclip::Interpolations.style(attachment, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reinterpolate :url" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:options).returns({:url => ":id"})
|
||||||
|
attachment.expects(:url).with(:style, false).returns("1234")
|
||||||
|
assert_equal "1234", Paperclip::Interpolations.url(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "raise if infinite loop detcted reinterpolating :url" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:options).returns({:url => ":url"})
|
||||||
|
assert_raises(Paperclip::InfiniteInterpolationError){ Paperclip::Interpolations.url(attachment, :style) }
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the filename as basename.extension" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:styles).returns({})
|
||||||
|
attachment.expects(:original_filename).returns("one.jpg").times(3)
|
||||||
|
assert_equal "one.jpg", Paperclip::Interpolations.filename(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the filename as basename.extension when format supplied" do
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:styles).returns({:style => {:format => :png}})
|
||||||
|
attachment.expects(:original_filename).returns("one.jpg").times(2)
|
||||||
|
assert_equal "one.png", Paperclip::Interpolations.filename(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the timestamp" do
|
||||||
|
now = Time.now
|
||||||
|
attachment = mock
|
||||||
|
attachment.expects(:instance_read).with(:updated_at).returns(now)
|
||||||
|
assert_equal now.to_s, Paperclip::Interpolations.timestamp(attachment, :style)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "call all expected interpolations with the given arguments" do
|
||||||
|
Paperclip::Interpolations.expects(:id).with(:attachment, :style).returns(1234)
|
||||||
|
Paperclip::Interpolations.expects(:attachment).with(:attachment, :style).returns("attachments")
|
||||||
|
Paperclip::Interpolations.expects(:notreal).never
|
||||||
|
value = Paperclip::Interpolations.interpolate(":notreal/:id/:attachment", :attachment, :style)
|
||||||
|
assert_equal ":notreal/1234/attachments", value
|
||||||
|
end
|
||||||
|
end
|
||||||
71
test/iostream_test.rb
Normal file
71
test/iostream_test.rb
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class IOStreamTest < Test::Unit::TestCase
|
||||||
|
context "IOStream" do
|
||||||
|
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
|
||||||
|
setup do
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
context "that is sent #stream_to" do
|
||||||
|
|
||||||
|
context "and given a String" do
|
||||||
|
setup do
|
||||||
|
FileUtils.mkdir_p(File.join(ROOT, 'tmp'))
|
||||||
|
assert @result = @file.stream_to(File.join(ROOT, 'tmp', 'iostream.string.test'))
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return a File" do
|
||||||
|
assert @result.is_a?(File)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "contain the same data as the original file" do
|
||||||
|
@file.rewind; @result.rewind
|
||||||
|
assert_equal @file.read, @result.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and given a Tempfile" do
|
||||||
|
setup do
|
||||||
|
tempfile = Tempfile.new('iostream.test')
|
||||||
|
tempfile.binmode
|
||||||
|
assert @result = @file.stream_to(tempfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return a Tempfile" do
|
||||||
|
assert @result.is_a?(Tempfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "contain the same data as the original file" do
|
||||||
|
@file.rewind; @result.rewind
|
||||||
|
assert_equal @file.read, @result.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "that is sent #to_tempfile" do
|
||||||
|
setup do
|
||||||
|
assert @tempfile = @file.to_tempfile
|
||||||
|
end
|
||||||
|
|
||||||
|
should "convert it to a Tempfile" do
|
||||||
|
assert @tempfile.is_a?(Tempfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have the Tempfile contain the same data as the file" do
|
||||||
|
@file.rewind; @tempfile.rewind
|
||||||
|
assert_equal @file.read, @tempfile.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
test/matchers/have_attached_file_matcher_test.rb
Normal file
21
test/matchers/have_attached_file_matcher_test.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class HaveAttachedFileMatcherTest < Test::Unit::TestCase
|
||||||
|
context "have_attached_file" do
|
||||||
|
setup do
|
||||||
|
@dummy_class = reset_class "Dummy"
|
||||||
|
reset_table "dummies"
|
||||||
|
@matcher = self.class.have_attached_file(:avatar)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reject a class with no attachment" do
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "accept a class with an attachment" do
|
||||||
|
modify_table("dummies"){|d| d.string :avatar_file_name }
|
||||||
|
@dummy_class.has_attached_file :avatar
|
||||||
|
assert_accepts @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase
|
||||||
|
context "validate_attachment_content_type" do
|
||||||
|
setup do
|
||||||
|
reset_table("dummies") do |d|
|
||||||
|
d.string :avatar_file_name
|
||||||
|
end
|
||||||
|
@dummy_class = reset_class "Dummy"
|
||||||
|
@dummy_class.has_attached_file :avatar
|
||||||
|
@matcher = self.class.validate_attachment_content_type(:avatar).
|
||||||
|
allowing(%w(image/png image/jpeg)).
|
||||||
|
rejecting(%w(audio/mp3 application/octet-stream))
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reject a class with no validation" do
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reject a class with a validation that doesn't match" do
|
||||||
|
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{audio/.*}
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "accept a class with a validation" do
|
||||||
|
@dummy_class.validates_attachment_content_type :avatar, :content_type => %r{image/.*}
|
||||||
|
assert_accepts @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
test/matchers/validate_attachment_presence_matcher_test.rb
Normal file
21
test/matchers/validate_attachment_presence_matcher_test.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase
|
||||||
|
context "validate_attachment_presence" do
|
||||||
|
setup do
|
||||||
|
reset_table("dummies"){|d| d.string :avatar_file_name }
|
||||||
|
@dummy_class = reset_class "Dummy"
|
||||||
|
@dummy_class.has_attached_file :avatar
|
||||||
|
@matcher = self.class.validate_attachment_presence(:avatar)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reject a class with no validation" do
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "accept a class with a validation" do
|
||||||
|
@dummy_class.validates_attachment_presence :avatar
|
||||||
|
assert_accepts @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
50
test/matchers/validate_attachment_size_matcher_test.rb
Normal file
50
test/matchers/validate_attachment_size_matcher_test.rb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase
|
||||||
|
context "validate_attachment_size" do
|
||||||
|
setup do
|
||||||
|
reset_table("dummies") do |d|
|
||||||
|
d.string :avatar_file_name
|
||||||
|
end
|
||||||
|
@dummy_class = reset_class "Dummy"
|
||||||
|
@dummy_class.has_attached_file :avatar
|
||||||
|
end
|
||||||
|
|
||||||
|
context "of limited size" do
|
||||||
|
setup{ @matcher = self.class.validate_attachment_size(:avatar).in(256..1024) }
|
||||||
|
|
||||||
|
should "reject a class with no validation" do
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reject a class with a validation that's too high" do
|
||||||
|
@dummy_class.validates_attachment_size :avatar, :in => 256..2048
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "reject a class with a validation that's too low" do
|
||||||
|
@dummy_class.validates_attachment_size :avatar, :in => 0..1024
|
||||||
|
assert_rejects @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "accept a class with a validation that matches" do
|
||||||
|
@dummy_class.validates_attachment_size :avatar, :in => 256..1024
|
||||||
|
assert_accepts @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "validates_attachment_size with infinite range" do
|
||||||
|
setup{ @matcher = self.class.validate_attachment_size(:avatar) }
|
||||||
|
|
||||||
|
should "accept a class with an upper limit" do
|
||||||
|
@dummy_class.validates_attachment_size :avatar, :less_than => 1
|
||||||
|
assert_accepts @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
|
||||||
|
should "accept a class with no upper limit" do
|
||||||
|
@dummy_class.validates_attachment_size :avatar, :greater_than => 1
|
||||||
|
assert_accepts @matcher, @dummy_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
291
test/paperclip_test.rb
Normal file
291
test/paperclip_test.rb
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class PaperclipTest < Test::Unit::TestCase
|
||||||
|
[:image_magick_path, :convert_path].each do |path|
|
||||||
|
context "Calling Paperclip.run with an #{path} specified" do
|
||||||
|
setup do
|
||||||
|
Paperclip.options[:image_magick_path] = nil
|
||||||
|
Paperclip.options[:convert_path] = nil
|
||||||
|
Paperclip.options[path] = "/usr/bin"
|
||||||
|
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[:convert_path] = nil
|
||||||
|
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
|
||||||
|
|
||||||
|
should "log the command when :log_command is set" do
|
||||||
|
Paperclip.options[:log_command] = true
|
||||||
|
Paperclip.expects(:bit_bucket).returns("/dev/null")
|
||||||
|
Paperclip.expects(:log).with("this is the command 2>/dev/null")
|
||||||
|
Paperclip.expects(:"`").with("this is the command 2>/dev/null")
|
||||||
|
Paperclip.run("this","is the command")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "raise when sent #processor and the name of a class that exists but isn't a subclass of Processor" do
|
||||||
|
assert_raises(Paperclip::PaperclipError){ Paperclip.processor(:attachment) }
|
||||||
|
end
|
||||||
|
|
||||||
|
should "raise when sent #processor and the name of a class that doesn't exist" do
|
||||||
|
assert_raises(NameError){ Paperclip.processor(:boogey_man) }
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return a class when sent #processor and the name of a class under Paperclip" do
|
||||||
|
assert_equal ::Paperclip::Thumbnail, Paperclip.processor(:thumbnail)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "call a proc sent to check_guard" do
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.expects(:one).returns(:one)
|
||||||
|
assert_equal :one, @dummy.avatar.send(:check_guard, lambda{|x| x.one })
|
||||||
|
end
|
||||||
|
|
||||||
|
should "call a method name sent to check_guard" do
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.expects(:one).returns(:one)
|
||||||
|
assert_equal :one, @dummy.avatar.send(:check_guard, :one)
|
||||||
|
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
|
||||||
|
|
||||||
|
context "on systems with /dev/null" do
|
||||||
|
setup do
|
||||||
|
File.expects(:exists?).with("/dev/null").returns(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return '/dev/null'" do
|
||||||
|
assert_equal "/dev/null", Paperclip.bit_bucket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An ActiveRecord model with an 'avatar' attachment" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :path => "tmp/:class/omg/:style.:extension"
|
||||||
|
@file = File.new(File.join(FIXTURES_DIR, "5k.png"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "not error when trying to also create a 'blah' attachment" do
|
||||||
|
assert_nothing_raised do
|
||||||
|
Dummy.class_eval do
|
||||||
|
has_attached_file :blah
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "that is attr_protected" do
|
||||||
|
setup do
|
||||||
|
Dummy.class_eval do
|
||||||
|
attr_protected :avatar
|
||||||
|
end
|
||||||
|
@dummy = Dummy.new
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not assign the avatar on mass-set" do
|
||||||
|
@dummy.attributes = { :other => "I'm set!",
|
||||||
|
:avatar => @file }
|
||||||
|
|
||||||
|
assert_equal "I'm set!", @dummy.other
|
||||||
|
assert ! @dummy.avatar?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "still allow assigment on normal set" do
|
||||||
|
@dummy.other = "I'm set!"
|
||||||
|
@dummy.avatar = @file
|
||||||
|
|
||||||
|
assert_equal "I'm set!", @dummy.other
|
||||||
|
assert @dummy.avatar?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a subclass" do
|
||||||
|
setup do
|
||||||
|
class ::SubDummy < Dummy; end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to use the attachment from the subclass" do
|
||||||
|
assert_nothing_raised do
|
||||||
|
@subdummy = SubDummy.create(:avatar => @file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to see the attachment definition from the subclass's class" do
|
||||||
|
assert_equal "tmp/:class/omg/:style.:extension", SubDummy.attachment_definitions[:avatar][:path]
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
Object.send(:remove_const, "SubDummy") rescue nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have an #avatar method" do
|
||||||
|
assert Dummy.new.respond_to?(:avatar)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have an #avatar= method" do
|
||||||
|
assert Dummy.new.respond_to?(:avatar=)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "that is valid" do
|
||||||
|
setup do
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be valid" do
|
||||||
|
assert @dummy.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
context "then has a validation added that makes it invalid" do
|
||||||
|
setup do
|
||||||
|
assert @dummy.save
|
||||||
|
Dummy.class_eval do
|
||||||
|
validates_attachment_content_type :avatar, :content_type => ["text/plain"]
|
||||||
|
end
|
||||||
|
@dummy2 = Dummy.find(@dummy.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be invalid when reloaded" do
|
||||||
|
assert ! @dummy2.valid?, @dummy2.errors.inspect
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be able to call #valid? twice without having duplicate errors" do
|
||||||
|
@dummy2.avatar.valid?
|
||||||
|
first_errors = @dummy2.avatar.errors
|
||||||
|
@dummy2.avatar.valid?
|
||||||
|
assert_equal first_errors, @dummy2.avatar.errors
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "a validation with an if guard clause" do
|
||||||
|
setup do
|
||||||
|
Dummy.send(:"validates_attachment_presence", :avatar, :if => lambda{|i| i.foo })
|
||||||
|
@dummy = Dummy.new
|
||||||
|
end
|
||||||
|
|
||||||
|
should "attempt validation if the guard returns true" do
|
||||||
|
@dummy.expects(:foo).returns(true)
|
||||||
|
@dummy.avatar.expects(:validate_presence).returns(nil)
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not attempt validation if the guard returns false" do
|
||||||
|
@dummy.expects(:foo).returns(false)
|
||||||
|
@dummy.avatar.expects(:validate_presence).never
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "a validation with an unless guard clause" do
|
||||||
|
setup do
|
||||||
|
Dummy.send(:"validates_attachment_presence", :avatar, :unless => lambda{|i| i.foo })
|
||||||
|
@dummy = Dummy.new
|
||||||
|
end
|
||||||
|
|
||||||
|
should "attempt validation if the guard returns true" do
|
||||||
|
@dummy.expects(:foo).returns(false)
|
||||||
|
@dummy.avatar.expects(:validate_presence).returns(nil)
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not attempt validation if the guard returns false" do
|
||||||
|
@dummy.expects(:foo).returns(true)
|
||||||
|
@dummy.avatar.expects(:validate_presence).never
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.should_validate validation, options, valid_file, invalid_file
|
||||||
|
context "with #{validation} validation and #{options.inspect} options" do
|
||||||
|
setup do
|
||||||
|
Dummy.send(:"validates_attachment_#{validation}", :avatar, options)
|
||||||
|
@dummy = Dummy.new
|
||||||
|
end
|
||||||
|
context "and assigning nil" do
|
||||||
|
setup do
|
||||||
|
@dummy.avatar = nil
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
if validation == :presence
|
||||||
|
should "have an error on the attachment" do
|
||||||
|
assert @dummy.errors.on(:avatar)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
should "not have an error on the attachment" do
|
||||||
|
assert_nil @dummy.errors.on(:avatar)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "and assigned a valid file" do
|
||||||
|
setup do
|
||||||
|
@dummy.avatar = valid_file
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
should "not have an error when assigned a valid file" do
|
||||||
|
assert ! @dummy.avatar.errors.key?(validation)
|
||||||
|
end
|
||||||
|
should "not have an error on the attachment" do
|
||||||
|
assert_nil @dummy.errors.on(:avatar)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "and assigned an invalid file" do
|
||||||
|
setup do
|
||||||
|
@dummy.avatar = invalid_file
|
||||||
|
@dummy.valid?
|
||||||
|
end
|
||||||
|
should "have an error when assigned a valid file" do
|
||||||
|
assert_not_nil @dummy.avatar.errors[validation]
|
||||||
|
end
|
||||||
|
should "have an error on the attachment" do
|
||||||
|
assert @dummy.errors.on(:avatar)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[[:presence, {}, "5k.png", nil],
|
||||||
|
[:size, {:in => 1..10240}, nil, "12k.png"],
|
||||||
|
[:size, {:less_than => 10240}, "5k.png", "12k.png"],
|
||||||
|
[:size, {:greater_than => 8096}, "12k.png", "5k.png"],
|
||||||
|
[:content_type, {:content_type => "image/png"}, "5k.png", "text.txt"],
|
||||||
|
[:content_type, {:content_type => "text/plain"}, "text.txt", "5k.png"],
|
||||||
|
[:content_type, {:content_type => %r{image/.*}}, "5k.png", "text.txt"]].each do |args|
|
||||||
|
validation, options, valid_file, invalid_file = args
|
||||||
|
valid_file &&= File.open(File.join(FIXTURES_DIR, valid_file), "rb")
|
||||||
|
invalid_file &&= File.open(File.join(FIXTURES_DIR, invalid_file), "rb")
|
||||||
|
|
||||||
|
should_validate validation, options, valid_file, invalid_file
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
10
test/processor_test.rb
Normal file
10
test/processor_test.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class ProcessorTest < Test::Unit::TestCase
|
||||||
|
should "instantiate and call #make when sent #make to the class" do
|
||||||
|
processor = mock
|
||||||
|
processor.expects(:make).with()
|
||||||
|
Paperclip::Processor.expects(:new).with(:one, :two, :three).returns(processor)
|
||||||
|
Paperclip::Processor.make(:one, :two, :three)
|
||||||
|
end
|
||||||
|
end
|
||||||
282
test/storage_test.rb
Normal file
282
test/storage_test.rb
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class StorageTest < Test::Unit::TestCase
|
||||||
|
context "Parsing S3 credentials" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :s3,
|
||||||
|
:bucket => "testing",
|
||||||
|
:s3_credentials => {:not => :important}
|
||||||
|
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@avatar = @dummy.avatar
|
||||||
|
|
||||||
|
@current_env = Rails.env
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
Object.const_set("Rails.env", @current_env)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "get the correct credentials when Rails.env is production" do
|
||||||
|
Object.const_set('Rails.env', "production")
|
||||||
|
assert_equal({:key => "12345"},
|
||||||
|
@avatar.parse_credentials('production' => {:key => '12345'},
|
||||||
|
:development => {:key => "54321"}))
|
||||||
|
end
|
||||||
|
|
||||||
|
should "get the correct credentials when Rails.env is development" do
|
||||||
|
Object.const_set('Rails.env', "development")
|
||||||
|
assert_equal({:key => "54321"},
|
||||||
|
@avatar.parse_credentials('production' => {:key => '12345'},
|
||||||
|
:development => {:key => "54321"}))
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return the argument if the key does not exist" do
|
||||||
|
Object.const_set('Rails.env', "not really an env")
|
||||||
|
assert_equal({:test => "12345"}, @avatar.parse_credentials(:test => "12345"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :s3,
|
||||||
|
:s3_credentials => {},
|
||||||
|
:bucket => "bucket",
|
||||||
|
:path => ":attachment/:basename.:extension",
|
||||||
|
:url => ":s3_path_url"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.avatar = StringIO.new(".")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return a url based on an S3 path" do
|
||||||
|
assert_match %r{^http://s3.amazonaws.com/bucket/avatars/stringio.txt}, @dummy.avatar.url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :s3,
|
||||||
|
:s3_credentials => {},
|
||||||
|
:bucket => "bucket",
|
||||||
|
:path => ":attachment/:basename.:extension",
|
||||||
|
:url => ":s3_domain_url"
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@dummy.avatar = StringIO.new(".")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return a url based on an S3 subdomain" do
|
||||||
|
assert_match %r{^http://bucket.s3.amazonaws.com/avatars/stringio.txt}, @dummy.avatar.url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :s3,
|
||||||
|
:s3_credentials => {
|
||||||
|
:production => { :bucket => "prod_bucket" },
|
||||||
|
:development => { :bucket => "dev_bucket" }
|
||||||
|
},
|
||||||
|
:s3_host_alias => "something.something.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://something.something.com/avatars/stringio.txt}, @dummy.avatar.url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Parsing S3 credentials with a bucket in them" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :s3,
|
||||||
|
:s3_credentials => {
|
||||||
|
:production => { :bucket => "prod_bucket" },
|
||||||
|
:development => { :bucket => "dev_bucket" }
|
||||||
|
}
|
||||||
|
@dummy = Dummy.new
|
||||||
|
@old_env = Rails.env
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown{ Object.const_set("Rails.env", @old_env) }
|
||||||
|
|
||||||
|
should "get the right bucket in production" do
|
||||||
|
Object.const_set("Rails.env", "production")
|
||||||
|
assert_equal "prod_bucket", @dummy.avatar.bucket_name
|
||||||
|
end
|
||||||
|
|
||||||
|
should "get the right bucket in development" do
|
||||||
|
Object.const_set("Rails.env", "development")
|
||||||
|
assert_equal "dev_bucket", @dummy.avatar.bucket_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with S3 storage" 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
|
||||||
|
|
||||||
|
should "be extended by the S3 module" do
|
||||||
|
assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not be extended by the Filesystem module" do
|
||||||
|
assert ! Dummy.new.avatar.is_a?(Paperclip::Storage::Filesystem)
|
||||||
|
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 }
|
||||||
|
|
||||||
|
should "not get a bucket to get a URL" do
|
||||||
|
@dummy.avatar.expects(:s3).never
|
||||||
|
@dummy.avatar.expects(:s3_bucket).never
|
||||||
|
assert_match %r{^http://s3\.amazonaws\.com/testing/avatars/original/5k\.png}, @dummy.avatar.url
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and saved" do
|
||||||
|
setup do
|
||||||
|
@s3_mock = stub
|
||||||
|
@bucket_mock = stub
|
||||||
|
RightAws::S3.expects(:new).with("12345", "54321", {}).returns(@s3_mock)
|
||||||
|
@s3_mock.expects(:bucket).with("testing", true, "public-read").returns(@bucket_mock)
|
||||||
|
@key_mock = stub
|
||||||
|
@bucket_mock.expects(:key).returns(@key_mock)
|
||||||
|
@key_mock.expects(:data=)
|
||||||
|
@key_mock.expects(:put).with(nil, 'public-read', 'Content-type' => 'image/png')
|
||||||
|
@dummy.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "succeed" do
|
||||||
|
assert true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and remove" do
|
||||||
|
setup do
|
||||||
|
@s3_mock = stub
|
||||||
|
@bucket_mock = stub
|
||||||
|
RightAws::S3.expects(:new).with("12345", "54321", {}).returns(@s3_mock)
|
||||||
|
@s3_mock.expects(:bucket).with("testing", true, "public-read").returns(@bucket_mock)
|
||||||
|
@key_mock = stub
|
||||||
|
@bucket_mock.expects(:key).at_least(2).returns(@key_mock)
|
||||||
|
@key_mock.expects(:delete)
|
||||||
|
@dummy.destroy_attached_files
|
||||||
|
end
|
||||||
|
|
||||||
|
should "succeed" do
|
||||||
|
assert true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with S3 storage and bucket defined as a Proc" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :storage => :s3,
|
||||||
|
:bucket => lambda { |attachment| "bucket_#{attachment.instance.other}" },
|
||||||
|
:s3_credentials => {:not => :important}
|
||||||
|
end
|
||||||
|
|
||||||
|
should "get the right bucket name" do
|
||||||
|
assert "bucket_a", Dummy.new(:other => 'a').avatar.bucket_name
|
||||||
|
assert "bucket_b", Dummy.new(:other => 'b').avatar.bucket_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An attachment with S3 storage and specific s3 headers 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_headers => {'Cache-Control' => 'max-age=31557600'}
|
||||||
|
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
|
||||||
|
@s3_mock = stub
|
||||||
|
@bucket_mock = stub
|
||||||
|
RightAws::S3.expects(:new).with("12345", "54321", {}).returns(@s3_mock)
|
||||||
|
@s3_mock.expects(:bucket).with("testing", true, "public-read").returns(@bucket_mock)
|
||||||
|
@key_mock = stub
|
||||||
|
@bucket_mock.expects(:key).returns(@key_mock)
|
||||||
|
@key_mock.expects(:data=)
|
||||||
|
@key_mock.expects(:put).with(nil,
|
||||||
|
'public-read',
|
||||||
|
'Content-type' => 'image/png',
|
||||||
|
'Cache-Control' => 'max-age=31557600')
|
||||||
|
@dummy.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "succeed" do
|
||||||
|
assert true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless ENV["S3_TEST_BUCKET"].blank?
|
||||||
|
context "Using S3 for real, an attachment with S3 storage" do
|
||||||
|
setup do
|
||||||
|
rebuild_model :styles => { :thumb => "100x100", :square => "32x32#" },
|
||||||
|
:storage => :s3,
|
||||||
|
:bucket => ENV["S3_TEST_BUCKET"],
|
||||||
|
:path => ":class/:attachment/:id/:style.:extension",
|
||||||
|
:s3_credentials => File.new(File.join(File.dirname(__FILE__), "s3.yml"))
|
||||||
|
|
||||||
|
Dummy.delete_all
|
||||||
|
@dummy = Dummy.new
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be extended by the S3 module" do
|
||||||
|
assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when assigned" do
|
||||||
|
setup do
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb')
|
||||||
|
@dummy.avatar = @file
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "still return a Tempfile when sent #to_io" do
|
||||||
|
assert_equal Tempfile, @dummy.avatar.to_io.class
|
||||||
|
end
|
||||||
|
|
||||||
|
context "and saved" do
|
||||||
|
setup do
|
||||||
|
@dummy.save
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be on S3" do
|
||||||
|
assert true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
177
test/thumbnail_test.rb
Normal file
177
test/thumbnail_test.rb
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
require 'test/helper'
|
||||||
|
|
||||||
|
class ThumbnailTest < Test::Unit::TestCase
|
||||||
|
|
||||||
|
context "A Paperclip Tempfile" do
|
||||||
|
setup do
|
||||||
|
@tempfile = Paperclip::Tempfile.new("file.jpg")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have its path contain a real extension" do
|
||||||
|
assert_equal ".jpg", File.extname(@tempfile.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be a real Tempfile" do
|
||||||
|
assert @tempfile.is_a?(::Tempfile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Another Paperclip Tempfile" do
|
||||||
|
setup do
|
||||||
|
@tempfile = Paperclip::Tempfile.new("file")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not have an extension if not given one" do
|
||||||
|
assert_equal "", File.extname(@tempfile.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "still be a real Tempfile" do
|
||||||
|
assert @tempfile.is_a?(::Tempfile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "An image" do
|
||||||
|
setup do
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
[["600x600>", "434x66"],
|
||||||
|
["400x400>", "400x61"],
|
||||||
|
["32x32<", "434x66"]
|
||||||
|
].each do |args|
|
||||||
|
context "being thumbnailed with a geometry of #{args[0]}" do
|
||||||
|
setup do
|
||||||
|
@thumb = Paperclip::Thumbnail.new(@file, :geometry => args[0])
|
||||||
|
end
|
||||||
|
|
||||||
|
should "start with dimensions of 434x66" do
|
||||||
|
cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
|
||||||
|
assert_equal "434x66", `#{cmd}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report the correct target geometry" do
|
||||||
|
assert_equal args[0], @thumb.target_geometry.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when made" do
|
||||||
|
setup do
|
||||||
|
@thumb_result = @thumb.make
|
||||||
|
end
|
||||||
|
|
||||||
|
should "be the size we expect it to be" do
|
||||||
|
cmd = %Q[identify -format "%wx%h" "#{@thumb_result.path}"]
|
||||||
|
assert_equal args[1], `#{cmd}`.chomp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "being thumbnailed at 100x50 with cropping" do
|
||||||
|
setup do
|
||||||
|
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "100x50#")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report its correct current and target geometries" do
|
||||||
|
assert_equal "100x50#", @thumb.target_geometry.to_s
|
||||||
|
assert_equal "434x66", @thumb.current_geometry.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report its correct format" do
|
||||||
|
assert_nil @thumb.format
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have whiny turned on by default" do
|
||||||
|
assert @thumb.whiny
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have convert_options set to nil by default" do
|
||||||
|
assert_equal nil, @thumb.convert_options
|
||||||
|
end
|
||||||
|
|
||||||
|
should "send the right command to convert when sent #make" do
|
||||||
|
Paperclip.expects(:"`").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+".*?"}
|
||||||
|
end
|
||||||
|
@thumb.make
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create the thumbnail when sent #make" do
|
||||||
|
dst = @thumb.make
|
||||||
|
assert_match /100x50/, `identify "#{dst.path}"`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "being thumbnailed with convert options set" do
|
||||||
|
setup do
|
||||||
|
@thumb = Paperclip::Thumbnail.new(@file,
|
||||||
|
:geometry => "100x50#",
|
||||||
|
:convert_options => "-strip -depth 8")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have convert_options value set" do
|
||||||
|
assert_equal "-strip -depth 8", @thumb.convert_options
|
||||||
|
end
|
||||||
|
|
||||||
|
should "send the right command to convert when sent #make" do
|
||||||
|
Paperclip.expects(:"`").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+".*?"}
|
||||||
|
end
|
||||||
|
@thumb.make
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create the thumbnail when sent #make" do
|
||||||
|
dst = @thumb.make
|
||||||
|
assert_match /100x50/, `identify "#{dst.path}"`
|
||||||
|
end
|
||||||
|
|
||||||
|
context "redefined to have bad convert_options setting" do
|
||||||
|
setup do
|
||||||
|
@thumb = Paperclip::Thumbnail.new(@file,
|
||||||
|
:geometry => "100x50#",
|
||||||
|
:convert_options => "-this-aint-no-option")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "error when trying to create the thumbnail" do
|
||||||
|
assert_raises(Paperclip::PaperclipError) do
|
||||||
|
@thumb.make
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A multipage PDF" do
|
||||||
|
setup do
|
||||||
|
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "twopage.pdf"), 'rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown { @file.close }
|
||||||
|
|
||||||
|
should "start with two pages with dimensions 612x792" do
|
||||||
|
cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
|
||||||
|
assert_equal "612x792"*2, `#{cmd}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
context "being thumbnailed at 100x100 with cropping" do
|
||||||
|
setup do
|
||||||
|
@thumb = Paperclip::Thumbnail.new(@file, :geometry => "100x100#", :format => :png)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report its correct current and target geometries" do
|
||||||
|
assert_equal "100x100#", @thumb.target_geometry.to_s
|
||||||
|
assert_equal "612x792", @thumb.current_geometry.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
should "report its correct format" do
|
||||||
|
assert_equal :png, @thumb.format
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create the thumbnail when sent #make" do
|
||||||
|
dst = @thumb.make
|
||||||
|
assert_match /100x100/, `identify "#{dst.path}"`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user