Commit 5494e3cf by jyurek

Docs and some splitting up of files because the single one was annoying.

git-svn-id: https://svn.thoughtbot.com/plugins/paperclip/trunk@246 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa
parent f99b0fd5
=Paperclip
Paperclip is a lightweight attachment manager for ActiveRecord. It saves and manages your attachments, be they images or Word Docs, with one line of code. You can automatically thumbnail images as they're uploaded, and you don't have to worry about installing any ruby-specific libraries. You don't have to worry about compiling headaches with RMagick, concurrency issues and race conditions with MiniMagick, or unsupported image types with ImageScience. All you need is a working Image- or GraphicsMagick installation -- the +convert+ command is all you need.
Paperclip is a lightweight attachment manager for ActiveRecord. It saves and manages your attachments, be they images or Word Docs, very easily. You can automatically thumbnail images as they're uploaded, and you don't have to worry about installing any ruby-specific libraries. You don't have to worry about compiling or memory overhead headaches with RMagick, concurrency issues and race conditions with MiniMagick or Tempfiles, or unsupported image types with ImageScience. All you need is a working Image- or GraphicsMagick installation -- the +convert+ and +identify+ commands are all you need.
Paperclip uses the filesystem to save your files. You specify a root that the files will be saved to, and, if you're attaching images, any other sizes they need to be converted to, and they'll all be saved to the right place when your object saves.
Paperclip uses the filesystem to save your files. You specify a path that the files will be saved to, and, if you're attaching images, any other sizes they need to be converted to, and they'll all be saved to the right place when your object saves. The primary intent of Paperclip is to treat a file like any other attribute, as transparently as possible. As such, files will not be saved until the record is saved, errors will be placed on the record if there were any conversion problems or failed validations, and assigning an attachment is as easy as "record.attachment_name = file".
See the documentation for the +has_attached_file+ method for extensive details.
See the documentation for the +has_attached_file+ method for options.
==Usage
In your model:
class Photo < ActiveRecord::Base
has_attached_file :image, :thumbnails => { :medium => "300x300>", :thumb => "100x100>" }
class User < ActiveRecord::Base
has_attached_file :avatar, :thumbnails => { :medium => "300x300>", :thumb => "100x100>" }
end
In your edit and new views:
<% form_for :photo, @photo, :url => photo_path, :html => { :multipart => true } do |form| %>
<%= form.file_field :image %>
<% form_for :user, @user, :url => user_path, :html => { :multipart => true } do |form| %>
<%= form.file_field :avatar %>
<% end %>
In your controller:
def create
@photo = Photo.create( params[:photo] )
@user = User.create( params[:user][:avatar] )
end
In your show view:
<%= image_tag @photo.image_url %>
<%= image_tag @photo.image_url(:original) %>
<%= image_tag @photo.image_url(:medium) %>
<%= image_tag @photo.image_url(:thumb) %>
<%= image_tag @user.avatar.url %>
<%= image_tag @user.avatar.url(:original) %>
<%= image_tag @user.avatar.url(:medium) %>
<%= image_tag @user.avatar.url(:thumb) %>
module Paperclip
# == Attachment
# Handles all the file management for the attachment, including saving, loading, presenting URLs,
# and database storage.
class Attachment
attr_reader :name, :instance, :original_filename, :content_type, :original_file_size, :definition, :errors
def initialize active_record, name, definition
@instance = active_record
@definition = definition
@name = name
@errors = []
clear_files
@dirty = true
self.original_filename = @instance["#{name}_file_name"]
self.content_type = @instance["#{name}_content_type"]
self.original_file_size = @instance["#{name}_file_size"]
end
def assign uploaded_file
return queue_destroy if uploaded_file.nil?
return unless is_a_file? uploaded_file
self.original_filename = sanitize_filename(uploaded_file.original_filename)
self.content_type = uploaded_file.content_type
self.original_file_size = uploaded_file.size
self[:original] = uploaded_file.read
@dirty = true
if definition.attachment_type == :image
make_thumbnails_from(self[:original])
end
end
def [](style)
@files[style]
end
def []=(style, data)
@files[style] = data
end
def clear_files
@files = {}
definition.styles.each{|style, geo| @files[style] = nil }
@dirty = false
end
def for_attached_files
@files.each do |style, data|
yield style, data
end
end
def dirty?
@dirty
end
# Validations
def valid?
definition.validations.each do |validation, constraints|
send("validate_#{validation}", *constraints)
end
errors.uniq!.empty?
end
# ActiveRecord Callbacks
def save
write_attachment if dirty?
delete_attachment if @delete_on_save
@delete_on_save = false
clear_files
end
def destroy(complain = false)
returning true do
@delete_on_save = true
@complain_on_delete = complain
self.original_filename = nil
self.content_type = nil
self.original_file_size = nil
clear_files
end
end
def destroy!
delete_attachment if definition.delete_on_destroy
end
def url style = nil
style ||= definition.default_style
pattern = if original_filename && instance.id
definition.url
else
definition.missing_url
end
interpolate( style, pattern )
end
def read style = nil
style ||= definition.default_style
self[style] ? self[style] : read_attachment(style)
end
def validate_existence *constraints
definition.styles.keys.each do |style|
errors << "requires a valid #{style} file." unless attachment_exists?(style)
end
end
def validate_size *constraints
errors << "file too large. Must be under #{constraints.last} bytes." if original_file_size > constraints.last
errors << "file too small. Must be over #{constraints.first} bytes." if original_file_size <= constraints.first
end
protected
def write_attachment
ensure_directories
for_attached_files do |style, data|
File.open( file_name(style), "w" ) do |file|
file.rewind
file.write(data) if data
end
end
end
def read_attachment style = nil
IO.read(file_name(style))
end
def delete_attachment complain = false
for_attached_files do |style, data|
file_path = file_name(style)
begin
FileUtils.rm file_path if file_path
rescue SystemCallError => e
raise PaperclipError, "could not be deleted." if Paperclip.options[:whiny_deletes] || complain
end
end
end
def file_name style = nil
style ||= definition.default_style
interpolate( style, definition.path )
end
def attachment_exists?(style)
style ||= definition.default_style
dirty? ? self[style] : File.exists?( file_name(style) )
end
def ensure_directories
for_attached_files do |style, file|
dirname = File.dirname( file_name(style) )
FileUtils.mkdir_p dirname
end
end
# Image Methods
public
def make_thumbnails_from data
begin
definition.thumbnails.each do |style, geometry|
self[style] = Thumbnail.make(geometry, data)
end
rescue PaperclipError => e
errors << e.message
clear_files
self[:original] = data
end
end
# Helper Methods
public
def interpolations
@interpolations ||= {
:rails_root => lambda{|style| RAILS_ROOT },
:id => lambda{|style| self.instance.id },
:class => lambda{|style| self.instance.class.to_s.underscore.pluralize },
:style => lambda{|style| style.to_s },
:attachment => lambda{|style| self.name.to_s.pluralize },
:filename => lambda{|style| self.original_filename },
:basename => lambda{|style| self.original_filename.gsub(/\..*$/, "") },
:extension => lambda{|style| self.original_filename.gsub(/^.*./, "") }
}
end
def interpolate style, source
returning source.dup do |s|
interpolations.each do |key, proc|
s.gsub!(/:#{key}/){ proc.call(style) }
end
end
end
def original_filename= new_name
instance["#{name}_file_name"] = @original_filename = new_name
end
def content_type= new_type
instance["#{name}_content_type"] = @content_type = new_type
end
def original_file_size= new_size
instance["#{name}_file_size"] = @original_file_size = new_size
end
def to_s
url
end
protected
def is_a_file? data
[:content_type, :original_filename, :read].map do |meth|
data.respond_to? meth
end.all?
end
def sanitize_filename filename
File.basename(filename).gsub(/[^\w\.\_]/,'_')
end
end
end
\ No newline at end of file
module Paperclip
# Holds the options defined by a call to has_attached_file. If options are not defined here as methods
# they will still be found through +method_missing+. Default values can be modified by modifying the
# hash returned by AttachmentDefinition.defaults directly.
class AttachmentDefinition
def self.defaults
@defaults ||= {
:path => ":rails_root/public/:class/:attachment/:id/:style_:filename",
:url => "/:class/:attachment/:id/:style_:filename",
:missing_url => "/:class/:attachment/:style_missing.png",
:attachment_type => :image,
:thumbnails => {},
:delete_on_destroy => true,
:default_style => :original
}
end
def initialize name, options
@name = name
@options = AttachmentDefinition.defaults.merge options
end
def name
@name
end
# A hash of all styles of the attachment. Essentially all the thumbnails
# plus the original.
def styles
@styles ||= thumbnails.merge(:original => nil)
end
# A hash of all defined thumbnails for this attachment.
def thumbnails
@thumbnails ||= @options[:thumbnails] || {}
end
# A convenience method to insert validation options into the options hash
# after the attachment has been defined.
def validate thing, *constraints
@options[:"validate_#{thing}"] = (constraints.length == 1 ? constraints.first : constraints)
end
def validations
@validations ||= @options.inject({}) do |valids, opts|
key, val = opts
if (m = key.to_s.match(/^validates?_(.+)/))
valids[m[1].to_sym] = val
end
valids
end
end
# Any option passed in that does not explicitly appear in this class can be accessed through methods
# regardless, as they are caught by +method_missing+. This does mean that it's probably not a good idea,
# if you plan on extending Paperclip, to have an option that has the same name as a method on +Object+.
def method_missing meth, *args
@options[meth]
end
end
end
\ No newline at end of file
module Paperclip
class Thumbnail
attr_accessor :geometry, :data
def initialize geometry, data
@geometry, @data = geometry, data
end
def self.make geometry, data
new(geometry, data).make
end
def make
return data if geometry.nil?
operator = geometry[-1,1]
begin
scale_geometry = geometry
scale_geometry, crop_geometry = geometry_for_crop if operator == '#'
convert = Paperclip.path_for_command("convert")
command = "#{convert} - -scale '#{scale_geometry}' #{operator == '#' ? "-crop '#{crop_geometry}'" : ""} - 2>/dev/null"
thumb = piping data, :to => command
rescue Errno::EPIPE => e
raise PaperclipError, "could not be thumbnailed. Is ImageMagick or GraphicsMagick installed and available?"
rescue SystemCallError => e
raise PaperclipError, "could not be thumbnailed."
end
if Paperclip.options[:whiny_thumbnails] && !$?.success?
raise PaperclipError, "could not be thumbnailed because of an error with 'convert'."
end
thumb
end
def geometry_for_crop
identify = Paperclip.path_for_command("identify")
piping data, :to => "#{identify} - 2>/dev/null" do |pipeout|
dimensions = pipeout.split[2]
if dimensions && (match = dimensions.match(/(\d+)x(\d+)/))
src = match[1,2].map(&:to_f)
srch = src[0] > src[1]
dst = geometry.match(/(\d+)x(\d+)/)[1,2].map(&:to_f)
dsth = dst[0] > dst[1]
ar = src[0] / src[1]
scale_geometry, scale = if dst[0] == dst[1]
if srch
[ "x#{dst[1].to_i}", src[1] / dst[1] ]
else
[ "#{dst[0].to_i}x", src[0] / dst[0] ]
end
elsif dsth
[ "#{dst[0].to_i}x", src[0] / dst[0] ]
else
[ "x#{dst[1].to_i}", src[1] / dst[1] ]
end
crop_geometry = if dsth
"%dx%d+%d+%d" % [ dst[0], dst[1], 0, (src[1] / scale - dst[1]) / 2 ]
else
"%dx%d+%d+%d" % [ dst[0], dst[1], (src[0] / scale - dst[0]) / 2, 0 ]
end
[ scale_geometry, crop_geometry ]
else
raise PaperclipError, "does not contain a valid image."
end
end
end
def piping data, command, &block
self.class.piping(data, command, &block)
end
def self.piping data, command, &block
command = command[:to] if command.respond_to?(:[]) && command[:to]
block ||= lambda {|d| d }
IO.popen(command, "w+") do |io|
io.write(data)
io.close_write
block.call(io.read)
end
end
end
end
\ No newline at end of file
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
type = self.path.match(/\.(\w+)$/)[1] || "data"
case type
when "jpg", "png", "gif" then "image/#{type}"
when "txt", "csv", "xml", "html", "htm" then "text/#{type}"
else "x-application/#{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
\ No newline at end of file
......@@ -9,7 +9,7 @@ class TestAttachment < Test::Unit::TestCase
@attachment = Paperclip::Attachment.new(@dummy, "thing", @definition)
end
end
context "The class Foo" do
setup do
ActiveRecord::Base.connection.create_table :foos, :force => true do |table|
......@@ -24,7 +24,7 @@ class TestAttachment < Test::Unit::TestCase
Object.send(:remove_const, :Foo) rescue nil
class ::Foo < ActiveRecord::Base; end
end
context "with an image attached to :image" do
setup do
assert Foo.has_attached_file(:image)
......@@ -32,27 +32,27 @@ class TestAttachment < Test::Unit::TestCase
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
end
should "be able to have a file assigned with :image=" do
assert_equal "test_image.jpg", @foo.image.original_filename
assert_equal "image/jpg", @foo.image.content_type
end
should "be able to retrieve the data as a blob" do
@file.rewind
assert_equal @file.read, @foo.image.read
end
context "and saved" do
setup do
assert @foo.save
end
should "have no errors" do
assert @foo.image.errors.blank?
assert @foo.errors.blank?
end
should "have a file on the filesystem" do
assert @foo.image.send(:file_name)
assert File.file?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
......@@ -62,7 +62,7 @@ class TestAttachment < Test::Unit::TestCase
end
end
end
context "with an image with thumbnails attached to :image and saved" do
setup do
assert Foo.has_attached_file(:image, :thumbnails => {:small => "16x16", :medium => "100x100", :large => "250x250", :square => "32x32#"})
......@@ -71,7 +71,7 @@ class TestAttachment < Test::Unit::TestCase
assert_nothing_raised{ @foo.image = @file }
assert @foo.save
end
should "have no errors" do
assert @foo.image.errors.blank?, @foo.image.errors.inspect
assert @foo.errors.blank?
......@@ -89,7 +89,7 @@ class TestAttachment < Test::Unit::TestCase
assert_equal "/foos/images/1/#{style}_test_image.jpg", @foo.image.url(style)
end
end
should "produce the correct dimensions when each style is identified" do
assert_match /16x15/, `identify '#{@foo.image.send(:file_name, :small)}'`
assert_match /32x32/, `identify '#{@foo.image.send(:file_name, :square)}'`
......@@ -98,24 +98,40 @@ class TestAttachment < Test::Unit::TestCase
assert_match /405x375/, `identify '#{@foo.image.send(:file_name, :original)}'`
end
end
context "with an image with thumbnails attached to :image and a document attached to :document" do
end
context "with an invalid image with a square thumbnail attached to :image" do
setup do
assert Foo.has_attached_file(:image, :thumbnails => {:square => "32x32#"})
assert Foo.validates_attached_file(:image)
@foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
end
should "not save and should report errors from identify" do
assert !@foo.save
assert @foo.errors.on(:image)
assert @foo.errors.on(:image).any?{|e| e.match(/does not contain a valid image/) }, @foo.errors.on(:image)
end
end
context "with an invalid image attached to :image" do
setup do
assert Foo.has_attached_file(:image, :thumbnails => {:small => "16x16", :medium => "100x100", :large => "250x250", :square => "32x32#"})
assert Foo.has_attached_file(:image, :thumbnails => {:sorta_square => "32x32"})
assert Foo.validates_attached_file(:image)
@foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
end
should "not save" do
should "not save and should report errors from convert" do
assert !@foo.save
assert @foo.errors.on(:image)
assert @foo.errors.on(:image).any?{|e| e.match(/does not contain a valid image/) }
assert @foo.errors.on(:image).any?{|e| e.match(/because of an error/) }, @foo.errors.on(:image)
end
end
end
end
end
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment