Commit 1b8bb3c3 by Joost Baaij

Merge branch 'master' of git://github.com/thoughtbot/paperclip

parents 289d9cfb e23d7f51
...@@ -5,3 +5,6 @@ test/s3.yml ...@@ -5,3 +5,6 @@ test/s3.yml
public public
paperclip*.gem paperclip*.gem
capybara*.html capybara*.html
*.rbc
.bundle
*SPIKE*
appraise "rails2" do
gem "rails", "~>2.3.0"
end
appraise "rails3" do
gem "rails", "~>3.0.0"
end
source "http://rubygems.org"
gem "shoulda"
gem "mocha"
gem "rake"
gem "ruby-debug"
gem "aws-s3", :require => "aws/s3"
gem "sqlite3-ruby", "~>1.3.0"
gem "appraisal"
GEM
remote: http://rubygems.org/
specs:
appraisal (0.1)
bundler
rake
aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (3.0.0)
columnize (0.3.2)
linecache (0.43)
mime-types (1.16)
mocha (0.9.9)
rake
rake (0.8.7)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
shoulda (2.11.3)
sqlite3-ruby (1.3.2)
xml-simple (1.0.12)
PLATFORMS
ruby
DEPENDENCIES
appraisal
aws-s3
mocha
rake
ruby-debug
shoulda
sqlite3-ruby (~> 1.3.0)
...@@ -15,7 +15,13 @@ useful defaults. ...@@ -15,7 +15,13 @@ useful defaults.
See the documentation for +has_attached_file+ in Paperclip::ClassMethods for See the documentation for +has_attached_file+ in Paperclip::ClassMethods for
more detailed options. more detailed options.
The complete RDoc[http://rdoc.info/projects/thoughtbot/paperclip] is online. The complete RDoc[http://rdoc.info/gems/paperclip] is online.
==Installation
Include the gem in your Gemfile:
gem "paperclip", "~> 2.3"
==Installation ==Installation
......
require 'rubygems'
require 'appraisal'
require 'bundler/setup'
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
require 'rake/rdoctask' require 'rake/rdoctask'
...@@ -6,11 +10,11 @@ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib') ...@@ -6,11 +10,11 @@ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
require 'paperclip' require 'paperclip'
desc 'Default: run unit tests.' desc 'Default: run unit tests.'
task :default => [:clean, :test] task :default => [:clean, :all]
desc 'Test the paperclip plugin under all supported Rails versions.' desc 'Test the paperclip plugin under all supported Rails versions.'
task :all do |t| task :all do |t|
exec('rake RAILS_VERSION=2.1 && rake RAILS_VERSION=2.3 && rake RAILS_VERSION=3.0') exec('rake appraisal test')
end end
desc 'Test the paperclip plugin.' desc 'Test the paperclip plugin.'
......
# This file was generated by Appraisal
source "http://rubygems.org"
gem "ruby-debug"
gem "rails", "~>2.3.0"
gem "rake"
gem "sqlite3-ruby", "~>1.3.0"
gem "shoulda"
gem "mocha"
gem "aws-s3", {:require=>"aws/s3"}
gem "appraisal"
\ No newline at end of file
GEM
remote: http://rubygems.org/
specs:
actionmailer (2.3.10)
actionpack (= 2.3.10)
actionpack (2.3.10)
activesupport (= 2.3.10)
rack (~> 1.1.0)
activerecord (2.3.10)
activesupport (= 2.3.10)
activeresource (2.3.10)
activesupport (= 2.3.10)
activesupport (2.3.10)
appraisal (0.1)
bundler
rake
aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (3.0.0)
columnize (0.3.2)
linecache (0.43)
mime-types (1.16)
mocha (0.9.9)
rake
rack (1.1.0)
rails (2.3.10)
actionmailer (= 2.3.10)
actionpack (= 2.3.10)
activerecord (= 2.3.10)
activeresource (= 2.3.10)
activesupport (= 2.3.10)
rake (>= 0.8.3)
rake (0.8.7)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
shoulda (2.11.3)
sqlite3-ruby (1.3.2)
xml-simple (1.0.12)
PLATFORMS
ruby
DEPENDENCIES
appraisal
aws-s3
mocha
rails (~> 2.3.0)
rake
ruby-debug
shoulda
sqlite3-ruby (~> 1.3.0)
# This file was generated by Appraisal
source "http://rubygems.org"
gem "ruby-debug"
gem "rails", ">=3.0.3"
gem "rake"
gem "sqlite3-ruby", "~>1.3.0"
gem "shoulda"
gem "mocha"
gem "aws-s3", {:require=>"aws/s3"}
gem "appraisal"
\ No newline at end of file
GEM
remote: http://rubygems.org/
specs:
abstract (1.0.0)
actionmailer (3.0.3)
actionpack (= 3.0.3)
mail (~> 2.2.9)
actionpack (3.0.3)
activemodel (= 3.0.3)
activesupport (= 3.0.3)
builder (~> 2.1.2)
erubis (~> 2.6.6)
i18n (~> 0.4)
rack (~> 1.2.1)
rack-mount (~> 0.6.13)
rack-test (~> 0.5.6)
tzinfo (~> 0.3.23)
activemodel (3.0.3)
activesupport (= 3.0.3)
builder (~> 2.1.2)
i18n (~> 0.4)
activerecord (3.0.3)
activemodel (= 3.0.3)
activesupport (= 3.0.3)
arel (~> 2.0.2)
tzinfo (~> 0.3.23)
activeresource (3.0.3)
activemodel (= 3.0.3)
activesupport (= 3.0.3)
activesupport (3.0.3)
appraisal (0.1)
bundler
rake
arel (2.0.4)
aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (2.1.2)
columnize (0.3.2)
erubis (2.6.6)
abstract (>= 1.0.0)
i18n (0.4.2)
linecache (0.43)
mail (2.2.10)
activesupport (>= 2.3.6)
i18n (~> 0.4.1)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.16)
mocha (0.9.9)
rake
polyglot (0.3.1)
rack (1.2.1)
rack-mount (0.6.13)
rack (>= 1.0.0)
rack-test (0.5.6)
rack (>= 1.0)
rails (3.0.3)
actionmailer (= 3.0.3)
actionpack (= 3.0.3)
activerecord (= 3.0.3)
activeresource (= 3.0.3)
activesupport (= 3.0.3)
bundler (~> 1.0)
railties (= 3.0.3)
railties (3.0.3)
actionpack (= 3.0.3)
activesupport (= 3.0.3)
rake (>= 0.8.7)
thor (~> 0.14.4)
rake (0.8.7)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
shoulda (2.11.3)
sqlite3-ruby (1.3.2)
thor (0.14.6)
treetop (1.4.9)
polyglot (>= 0.3.1)
tzinfo (0.3.23)
xml-simple (1.0.12)
PLATFORMS
ruby
DEPENDENCIES
appraisal
aws-s3
mocha
rails (>= 3.0.3)
rake
ruby-debug
shoulda
sqlite3-ruby (~> 1.3.0)
...@@ -37,6 +37,7 @@ require 'paperclip/thumbnail' ...@@ -37,6 +37,7 @@ require 'paperclip/thumbnail'
require 'paperclip/interpolations' require 'paperclip/interpolations'
require 'paperclip/style' require 'paperclip/style'
require 'paperclip/attachment' require 'paperclip/attachment'
require 'paperclip/storage'
require 'paperclip/callback_compatability' require 'paperclip/callback_compatability'
require 'paperclip/command_line' require 'paperclip/command_line'
require 'paperclip/railtie' require 'paperclip/railtie'
...@@ -103,15 +104,6 @@ module Paperclip ...@@ -103,15 +104,6 @@ module Paperclip
CommandLine.new(cmd, *params).run CommandLine.new(cmd, *params).run
end end
def included base #:nodoc:
base.extend ClassMethods
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: def processor name #:nodoc:
name = name.to_s.camelize name = name.to_s.camelize
processor = Paperclip.const_get(name) processor = Paperclip.const_get(name)
...@@ -121,6 +113,12 @@ module Paperclip ...@@ -121,6 +113,12 @@ module Paperclip
processor processor
end end
def each_instance_with_attachment(klass, name)
Object.const_get(klass).all.each do |instance|
yield(instance) if instance.send(:"#{name}?")
end
end
# Log a paperclip-specific line. Uses ActiveRecord::Base.logger # Log a paperclip-specific line. Uses ActiveRecord::Base.logger
# by default. Set Paperclip.options[:log] to false to turn off. # by default. Set Paperclip.options[:log] to false to turn off.
def log message def log message
...@@ -159,6 +157,17 @@ module Paperclip ...@@ -159,6 +157,17 @@ module Paperclip
class InfiniteInterpolationError < PaperclipError #:nodoc: class InfiniteInterpolationError < PaperclipError #:nodoc:
end end
module Glue
def self.included base #:nodoc:
base.extend ClassMethods
if base.respond_to?("set_callback")
base.send :include, Paperclip::CallbackCompatability::Rails3
else
base.send :include, Paperclip::CallbackCompatability::Rails21
end
end
end
module ClassMethods module ClassMethods
# +has_attached_file+ gives the class it is called on an attribute that maps to a file. This # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
# is typically a file stored somewhere on the filesystem and has been uploaded by a user. # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
......
...@@ -4,6 +4,7 @@ module Paperclip ...@@ -4,6 +4,7 @@ module Paperclip
# when the model saves, deletes when the model is destroyed, and processes # when the model saves, deletes when the model is destroyed, and processes
# the file upon assignment. # the file upon assignment.
class Attachment class Attachment
include IOStream
def self.default_options def self.default_options
@default_options ||= { @default_options ||= {
...@@ -88,11 +89,11 @@ module Paperclip ...@@ -88,11 +89,11 @@ module Paperclip
return nil if uploaded_file.nil? return nil if uploaded_file.nil?
@queued_for_write[:original] = uploaded_file.to_tempfile @queued_for_write[:original] = to_tempfile(uploaded_file)
instance_write(:file_name, uploaded_file.original_filename.strip) instance_write(:file_name, uploaded_file.original_filename.strip)
instance_write(:content_type, uploaded_file.content_type.to_s.strip) instance_write(:content_type, uploaded_file.content_type.to_s.strip)
instance_write(:file_size, uploaded_file.size.to_i) instance_write(:file_size, uploaded_file.size.to_i)
instance_write(:fingerprint, uploaded_file.fingerprint) instance_write(:fingerprint, generate_fingerprint(uploaded_file))
instance_write(:updated_at, Time.now) instance_write(:updated_at, Time.now)
@dirty = true @dirty = true
...@@ -101,7 +102,7 @@ module Paperclip ...@@ -101,7 +102,7 @@ module Paperclip
# Reset the file size if the original file was reprocessed. # Reset the file size if the original file was reprocessed.
instance_write(:file_size, @queued_for_write[:original].size.to_i) instance_write(:file_size, @queued_for_write[:original].size.to_i)
instance_write(:fingerprint, @queued_for_write[:original].fingerprint) instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
ensure ensure
uploaded_file.close if close_uploaded_file uploaded_file.close if close_uploaded_file
end end
...@@ -180,7 +181,7 @@ module Paperclip ...@@ -180,7 +181,7 @@ module Paperclip
# Returns the hash of the file as originally assigned, and lives in the # Returns the hash of the file as originally assigned, and lives in the
# <attachment>_fingerprint attribute of the model. # <attachment>_fingerprint attribute of the model.
def fingerprint def fingerprint
instance_read(:fingerprint) || (@queued_for_write[:original] && @queued_for_write[:original].fingerprint) instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
end end
# Returns the content_type of the file as originally assigned, and lives # Returns the content_type of the file as originally assigned, and lives
...@@ -196,6 +197,12 @@ module Paperclip ...@@ -196,6 +197,12 @@ module Paperclip
time && time.to_f.to_i time && time.to_f.to_i
end end
def generate_fingerprint(source)
data = source.read
source.rewind if source.respond_to?(:rewind)
Digest::MD5.hexdigest(data)
end
# Paths and URLs can have a number of variables interpolated into them # Paths and URLs can have a number of variables interpolated into them
# to vary the storage location based on name, id, style, class, etc. # to vary the storage location based on name, id, style, class, etc.
# This method is a deprecated access into supplying and retrieving these # This method is a deprecated access into supplying and retrieving these
...@@ -277,13 +284,12 @@ module Paperclip ...@@ -277,13 +284,12 @@ module Paperclip
end end
def initialize_storage #:nodoc: def initialize_storage #:nodoc:
storage_name = @storage.to_s.capitalize storage_class_name = @storage.to_s.capitalize
begin begin
require "paperclip/storage/#{@storage}" @storage_module = Paperclip::Storage.const_get(storage_class_name)
rescue MissingSourceFile rescue NameError
raise StorageMethodNotFound, "Cannot find the '#{@storage}' storage adapter." raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
end end
@storage_module = Paperclip::Storage.const_get(storage_name)
self.extend(@storage_module) self.extend(@storage_module)
end end
......
...@@ -8,7 +8,7 @@ module Paperclip ...@@ -8,7 +8,7 @@ module Paperclip
@binary = binary.dup @binary = binary.dup
@params = params.dup @params = params.dup
@options = options.dup @options = options.dup
@swallow_stderr = @options.delete(:swallow_stderr) @swallow_stderr = @options.has_key?(:swallow_stderr) ? @options.delete(:swallow_stderr) : Paperclip.options[:swallow_stderr]
@expected_outcodes = @options.delete(:expected_outcodes) @expected_outcodes = @options.delete(:expected_outcodes)
@expected_outcodes ||= [0] @expected_outcodes ||= [0]
end end
......
...@@ -41,8 +41,9 @@ module Paperclip ...@@ -41,8 +41,9 @@ module Paperclip
# Returns the interpolated URL. Will raise an error if the url itself # Returns the interpolated URL. Will raise an error if the url itself
# contains ":url" to prevent infinite recursion. This interpolation # contains ":url" to prevent infinite recursion. This interpolation
# is used in the default :path to ease default specifications. # is used in the default :path to ease default specifications.
RIGHT_HERE = "#{__FILE__.gsub(%r{^\./}, "")}:#{__LINE__ + 3}"
def url attachment, style_name def url attachment, style_name
raise InfiniteInterpolationError if caller.any?{|b| b.index("#{__FILE__}:#{__LINE__ + 1}") } raise InfiniteInterpolationError if caller.any?{|b| b.index(RIGHT_HERE) }
attachment.url(style_name, false) attachment.url(style_name, false)
end end
......
# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying # Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
# and Tempfile conversion. # and Tempfile conversion.
module IOStream module IOStream
# Returns a Tempfile containing the contents of the readable object. # Returns a Tempfile containing the contents of the readable object.
def to_tempfile def to_tempfile(object)
name = respond_to?(:original_filename) ? original_filename : (respond_to?(:path) ? path : "stream") return object.to_tempfile if object.respond_to?(:to_tempfile)
tempfile = Paperclip::Tempfile.new("stream" + File.extname(name)) name = object.respond_to?(:original_filename) ? object.original_filename : (object.respond_to?(:path) ? object.path : "stream")
tempfile = Paperclip::Tempfile.new(["stream", File.extname(name)])
tempfile.binmode tempfile.binmode
self.stream_to(tempfile) stream_to(object, tempfile)
end end
# Copies one read-able object from one place to another in blocks, obviating the need to load # Copies one read-able object from one place to another in blocks, obviating the need to load
# the whole thing into memory. Defaults to 8k blocks. If this module is included in both # the whole thing into memory. Defaults to 8k blocks. Returns a File if a String is passed
# StringIO and Tempfile, then either can have its data copied anywhere else without typing # in as the destination and returns the IO or Tempfile as passed in if one is sent as the destination.
# worries or memory overhead worries. Returns a File if a String is passed in as the destination def stream_to object, path_or_file, in_blocks_of = 8192
# and returns the IO or Tempfile as passed in if one is sent as the destination.
def stream_to path_or_file, in_blocks_of = 8192
dstio = case path_or_file dstio = case path_or_file
when String then File.new(path_or_file, "wb+") when String then File.new(path_or_file, "wb+")
when IO then path_or_file when IO then path_or_file
when Tempfile then path_or_file when Tempfile then path_or_file
end end
buffer = "" buffer = ""
self.rewind object.rewind
while self.read(in_blocks_of, buffer) do while object.read(in_blocks_of, buffer) do
dstio.write(buffer) dstio.write(buffer)
end end
dstio.rewind dstio.rewind
...@@ -31,18 +29,6 @@ module IOStream ...@@ -31,18 +29,6 @@ module IOStream
end end
end end
class IO #:nodoc:
include IOStream
end
%w( Tempfile StringIO ).each do |klass|
if Object.const_defined? klass
Object.const_get(klass).class_eval do
include IOStream
end
end
end
# Corrects a bug in Windows when asking for Tempfile size. # Corrects a bug in Windows when asking for Tempfile size.
if defined? Tempfile if defined? Tempfile
class Tempfile class Tempfile
......
...@@ -40,10 +40,19 @@ module Paperclip ...@@ -40,10 +40,19 @@ module Paperclip
# on this blog post: # on this blog post:
# http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
class Tempfile < ::Tempfile class Tempfile < ::Tempfile
# Replaces Tempfile's +make_tmpname+ with one that honors file extensions. # This is Ruby 1.8.7's implementation.
if RUBY_VERSION <= "1.8.6"
def make_tmpname(basename, n) def make_tmpname(basename, n)
extension = File.extname(basename) case basename
sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n.to_i, extension) when Array
prefix, suffix = *basename
else
prefix, suffix = basename, ''
end
t = Time.now.strftime("%y%m%d")
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}-#{n}#{suffix}"
end
end end
end end
end end
...@@ -17,7 +17,7 @@ module Paperclip ...@@ -17,7 +17,7 @@ module Paperclip
class Railtie class Railtie
def self.insert def self.insert
ActiveRecord::Base.send(:include, Paperclip) ActiveRecord::Base.send(:include, Paperclip::Glue)
File.send(:include, Paperclip::Upfile) File.send(:include, Paperclip::Upfile)
end end
end end
......
require "paperclip/storage/filesystem"
require "paperclip/storage/s3"
...@@ -56,6 +56,7 @@ module Paperclip ...@@ -56,6 +56,7 @@ module Paperclip
while(true) while(true)
path = File.dirname(path) path = File.dirname(path)
FileUtils.rmdir(path) FileUtils.rmdir(path)
break if File.exists?(path) # Ruby 1.9.2 does not raise if the removal failed.
end end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
# Stop trying to remove parent directories # Stop trying to remove parent directories
......
...@@ -127,12 +127,20 @@ module Paperclip ...@@ -127,12 +127,20 @@ module Paperclip
# style, in the format most representative of the current storage. # style, in the format most representative of the current storage.
def to_file style = default_style def to_file style = default_style
return @queued_for_write[style] if @queued_for_write[style] return @queued_for_write[style] if @queued_for_write[style]
file = Tempfile.new(path(style)) filename = path(style)
extname = File.extname(filename)
basename = File.basename(filename, extname)
file = Tempfile.new([basename, extname])
file.binmode
file.write(AWS::S3::S3Object.value(path(style), bucket_name)) file.write(AWS::S3::S3Object.value(path(style), bucket_name))
file.rewind file.rewind
return file return file
end end
def create_bucket
AWS::S3::Bucket.create(bucket_name)
end
def flush_writes #:nodoc: def flush_writes #:nodoc:
@queued_for_write.each do |style, file| @queued_for_write.each do |style, file|
begin begin
...@@ -143,6 +151,9 @@ module Paperclip ...@@ -143,6 +151,9 @@ module Paperclip
{:content_type => instance_read(:content_type), {:content_type => instance_read(:content_type),
:access => @s3_permissions, :access => @s3_permissions,
}.merge(@s3_headers)) }.merge(@s3_headers))
rescue AWS::S3::NoSuchBucket => e
create_bucket
retry
rescue AWS::S3::ResponseError => e rescue AWS::S3::ResponseError => e
raise raise
end end
......
...@@ -45,7 +45,7 @@ module Paperclip ...@@ -45,7 +45,7 @@ module Paperclip
# that contains the new image. # that contains the new image.
def make def make
src = @file src = @file
dst = Tempfile.new([@basename, @format].compact.join(".")) dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
dst.binmode dst.binmode
begin begin
......
...@@ -32,11 +32,6 @@ module Paperclip ...@@ -32,11 +32,6 @@ module Paperclip
def size def size
File.size(self) File.size(self)
end end
# Returns the hash of the file.
def fingerprint
Digest::MD5.hexdigest(self.read)
end
end end
end end
......
module Paperclip module Paperclip
VERSION = "2.3.3" unless defined? Paperclip::VERSION VERSION = "2.3.8" unless defined? Paperclip::VERSION
end end
def obtain_class def obtain_class
class_name = ENV['CLASS'] || ENV['class'] class_name = ENV['CLASS'] || ENV['class']
raise "Must specify CLASS" unless class_name raise "Must specify CLASS" unless class_name
@klass = Object.const_get(class_name) class_name
end end
def obtain_attachments def obtain_attachments(klass)
klass = Object.const_get(klass.to_s)
name = ENV['ATTACHMENT'] || ENV['attachment'] name = ENV['ATTACHMENT'] || ENV['attachment']
raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions) raise "Class #{klass.name} has no attachments specified" unless klass.respond_to?(:attachment_definitions)
if !name.blank? && @klass.attachment_definitions.keys.include?(name) if !name.blank? && klass.attachment_definitions.keys.include?(name)
[ name ] [ name ]
else else
@klass.attachment_definitions.keys klass.attachment_definitions.keys
end end
end end
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 namespace :paperclip do
desc "Refreshes both metadata and thumbnails." desc "Refreshes both metadata and thumbnails."
task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"] task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
...@@ -41,17 +23,23 @@ namespace :paperclip do ...@@ -41,17 +23,23 @@ namespace :paperclip do
desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)." desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
task :thumbnails => :environment do task :thumbnails => :environment do
errors = [] errors = []
for_all_attachments do |instance, name| klass = obtain_class
names = obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
result = instance.send(name).reprocess! result = instance.send(name).reprocess!
errors << [instance.id, instance.errors] unless instance.errors.blank? errors << [instance.id, instance.errors] unless instance.errors.blank?
result end
end end
errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" } errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
end end
desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)." desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
task :metadata => :environment do task :metadata => :environment do
for_all_attachments do |instance, name| klass = obtain_class
names = obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
if file = instance.send(name).to_file if file = instance.send(name).to_file
instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip) instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
instance.send("#{name}_content_type=", file.content_type.strip) instance.send("#{name}_content_type=", file.content_type.strip)
...@@ -63,10 +51,14 @@ namespace :paperclip do ...@@ -63,10 +51,14 @@ namespace :paperclip do
end end
end end
end end
end
desc "Cleans out invalid attachments. Useful after you've added new validations." desc "Cleans out invalid attachments. Useful after you've added new validations."
task :clean => :environment do task :clean => :environment do
for_all_attachments do |instance, name| klass = obtain_class
names = obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
instance.send(name).send(:validate) instance.send(name).send(:validate)
if instance.send(name).valid? if instance.send(name).valid?
true true
...@@ -76,4 +68,5 @@ namespace :paperclip do ...@@ -76,4 +68,5 @@ namespace :paperclip do
end end
end end
end end
end
end end
...@@ -28,6 +28,7 @@ spec = Gem::Specification.new do |s| ...@@ -28,6 +28,7 @@ spec = Gem::Specification.new do |s|
s.add_dependency 'activerecord' s.add_dependency 'activerecord'
s.add_dependency 'activesupport' s.add_dependency 'activesupport'
s.add_development_dependency 'shoulda' s.add_development_dependency 'shoulda'
s.add_development_dependency 'appraisal'
s.add_development_dependency 'mocha' s.add_development_dependency 'mocha'
s.add_development_dependency 'aws-s3' s.add_development_dependency 'aws-s3'
s.add_development_dependency 'sqlite3-ruby' s.add_development_dependency 'sqlite3-ruby'
......
# encoding: utf-8 # encoding: utf-8
require 'test/helper' require './test/helper'
class Dummy class Dummy
# This is a dummy class # This is a dummy class
...@@ -463,14 +463,11 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -463,14 +463,11 @@ class AttachmentTest < Test::Unit::TestCase
setup do setup do
rebuild_model rebuild_model
@not_file = mock @not_file = mock("not_file")
@tempfile = mock @tempfile = mock("tempfile")
@not_file.stubs(:nil?).returns(false) @not_file.stubs(:nil?).returns(false)
@not_file.stubs(:fingerprint).returns('bd94545193321376b70136f8b223abf8')
@tempfile.stubs(:fingerprint).returns('bd94545193321376b70136f8b223abf8')
@not_file.expects(:size).returns(10) @not_file.expects(:size).returns(10)
@tempfile.expects(:size).returns(10) @tempfile.expects(:size).returns(10)
@not_file.expects(:to_tempfile).returns(@tempfile)
@not_file.expects(:original_filename).returns("sheep_say_bæ.png\r\n") @not_file.expects(:original_filename).returns("sheep_say_bæ.png\r\n")
@not_file.expects(:content_type).returns("image/png\r\n") @not_file.expects(:content_type).returns("image/png\r\n")
...@@ -479,6 +476,9 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -479,6 +476,9 @@ class AttachmentTest < Test::Unit::TestCase
@attachment.expects(:valid_assignment?).with(@not_file).returns(true) @attachment.expects(:valid_assignment?).with(@not_file).returns(true)
@attachment.expects(:queue_existing_for_delete) @attachment.expects(:queue_existing_for_delete)
@attachment.expects(:post_process) @attachment.expects(:post_process)
@attachment.expects(:to_tempfile).returns(@tempfile)
@attachment.expects(:generate_fingerprint).with(@tempfile).returns("12345")
@attachment.expects(:generate_fingerprint).with(@not_file).returns("12345")
@dummy.avatar = @not_file @dummy.avatar = @not_file
end end
...@@ -606,7 +606,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -606,7 +606,7 @@ class AttachmentTest < Test::Unit::TestCase
[:large, :medium, :small].each do |style| [:large, :medium, :small].each do |style|
io = @attachment.to_file(style) io = @attachment.to_file(style)
# p "in commit to disk test, io is #{io.inspect} and @instance.id is #{@instance.id}" # p "in commit to disk test, io is #{io.inspect} and @instance.id is #{@instance.id}"
assert File.exists?(io) assert File.exists?(io.path)
assert ! io.is_a?(::Tempfile) assert ! io.is_a?(::Tempfile)
io.close io.close
end end
...@@ -703,7 +703,7 @@ class AttachmentTest < Test::Unit::TestCase ...@@ -703,7 +703,7 @@ class AttachmentTest < Test::Unit::TestCase
now = Time.now now = Time.now
Time.stubs(:now).returns(now) Time.stubs(:now).returns(now)
@dummy.avatar = @file @dummy.avatar = @file
assert now, @dummy.avatar.updated_at assert_equal now.to_i, @dummy.avatar.updated_at.to_i
end end
should "return nil when reloaded and sent #avatar_updated_at" do should "return nil when reloaded and sent #avatar_updated_at" do
......
require 'test/helper' require './test/helper'
class CommandLineTest < Test::Unit::TestCase class CommandLineTest < Test::Unit::TestCase
def setup def setup
...@@ -7,13 +7,13 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -7,13 +7,13 @@ class CommandLineTest < Test::Unit::TestCase
end end
should "take a command and parameters and produce a shell command for bash" do should "take a command and parameters and produce a shell command for bash" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png") cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
assert_equal "convert a.jpg b.png", cmd.command assert_equal "convert a.jpg b.png", cmd.command
end end
should "be able to set a path and produce commands with that path" do should "be able to set a path and produce commands with that path" do
Paperclip::CommandLine.path = "/opt/bin" Paperclip::CommandLine.path = "/opt/bin"
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png") cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
assert_equal "/opt/bin/convert a.jpg b.png", cmd.command assert_equal "/opt/bin/convert a.jpg b.png", cmd.command
end end
...@@ -21,7 +21,8 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -21,7 +21,8 @@ class CommandLineTest < Test::Unit::TestCase
cmd = Paperclip::CommandLine.new("convert", cmd = Paperclip::CommandLine.new("convert",
":one :{two}", ":one :{two}",
:one => "a.jpg", :one => "a.jpg",
:two => "b.png") :two => "b.png",
:swallow_stderr => false)
assert_equal "convert 'a.jpg' 'b.png'", cmd.command assert_equal "convert 'a.jpg' 'b.png'", cmd.command
end end
...@@ -30,7 +31,8 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -30,7 +31,8 @@ class CommandLineTest < Test::Unit::TestCase
cmd = Paperclip::CommandLine.new("convert", cmd = Paperclip::CommandLine.new("convert",
":one :{two}", ":one :{two}",
:one => "a.jpg", :one => "a.jpg",
:two => "b.png") :two => "b.png",
:swallow_stderr => false)
assert_equal 'convert "a.jpg" "b.png"', cmd.command assert_equal 'convert "a.jpg" "b.png"', cmd.command
end end
...@@ -38,7 +40,8 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -38,7 +40,8 @@ class CommandLineTest < Test::Unit::TestCase
cmd = Paperclip::CommandLine.new("convert", cmd = Paperclip::CommandLine.new("convert",
":one :two", ":one :two",
:one => "`rm -rf`.jpg", :one => "`rm -rf`.jpg",
:two => "ha'ha.png") :two => "ha'ha.png",
:swallow_stderr => false)
assert_equal "convert '`rm -rf`.jpg' 'ha'\\''ha.png'", cmd.command assert_equal "convert '`rm -rf`.jpg' 'ha'\\''ha.png'", cmd.command
end end
...@@ -47,7 +50,8 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -47,7 +50,8 @@ class CommandLineTest < Test::Unit::TestCase
cmd = Paperclip::CommandLine.new("convert", cmd = Paperclip::CommandLine.new("convert",
":one :two", ":one :two",
:one => "`rm -rf`.jpg", :one => "`rm -rf`.jpg",
:two => "ha'ha.png") :two => "ha'ha.png",
:swallow_stderr => false)
assert_equal %{convert "`rm -rf`.jpg" "ha'ha.png"}, cmd.command assert_equal %{convert "`rm -rf`.jpg" "ha'ha.png"}, cmd.command
end end
...@@ -80,7 +84,7 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -80,7 +84,7 @@ class CommandLineTest < Test::Unit::TestCase
end end
should "run the #command it's given and return the output" do should "run the #command it's given and return the output" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png") cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value) cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(0) do with_exitstatus_returning(0) do
assert_equal :correct_value, cmd.run assert_equal :correct_value, cmd.run
...@@ -88,7 +92,7 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -88,7 +92,7 @@ class CommandLineTest < Test::Unit::TestCase
end end
should "raise a PaperclipCommandLineError if the result code isn't expected" do should "raise a PaperclipCommandLineError if the result code isn't expected" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png") cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value) cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(1) do with_exitstatus_returning(1) do
assert_raises(Paperclip::PaperclipCommandLineError) do assert_raises(Paperclip::PaperclipCommandLineError) do
...@@ -100,7 +104,8 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -100,7 +104,8 @@ class CommandLineTest < Test::Unit::TestCase
should "not raise a PaperclipCommandLineError if the result code is expected" do should "not raise a PaperclipCommandLineError if the result code is expected" do
cmd = Paperclip::CommandLine.new("convert", cmd = Paperclip::CommandLine.new("convert",
"a.jpg b.png", "a.jpg b.png",
:expected_outcodes => [0, 1]) :expected_outcodes => [0, 1],
:swallow_stderr => false)
cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value) cmd.class.stubs(:"`").with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(1) do with_exitstatus_returning(1) do
assert_nothing_raised do assert_nothing_raised do
...@@ -110,7 +115,7 @@ class CommandLineTest < Test::Unit::TestCase ...@@ -110,7 +115,7 @@ class CommandLineTest < Test::Unit::TestCase
end end
should "log the command" do should "log the command" do
cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png") cmd = Paperclip::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.class.stubs(:'`') cmd.class.stubs(:'`')
Paperclip.expects(:log).with("convert a.jpg b.png") Paperclip.expects(:log).with("convert a.jpg b.png")
cmd.run cmd.run
......
require 'test/helper' require './test/helper'
class GeometryTest < Test::Unit::TestCase class GeometryTest < Test::Unit::TestCase
context "Paperclip::Geometry" do context "Paperclip::Geometry" do
......
...@@ -5,18 +5,6 @@ require 'test/unit' ...@@ -5,18 +5,6 @@ require 'test/unit'
require 'shoulda' require 'shoulda'
require 'mocha' require 'mocha'
case ENV['RAILS_VERSION']
when '2.1' then
gem 'activerecord', '~>2.1.0'
gem 'activesupport', '~>2.1.0'
when '3.0' then
gem 'activerecord', '~>3.0.0'
gem 'activesupport', '~>3.0.0'
else
gem 'activerecord', '~>2.3.0'
gem 'activesupport', '~>2.3.0'
end
require 'active_record' require 'active_record'
require 'active_record/version' require 'active_record/version'
require 'active_support' require 'active_support'
...@@ -53,7 +41,7 @@ $LOAD_PATH << File.join(ROOT, 'lib', 'paperclip') ...@@ -53,7 +41,7 @@ $LOAD_PATH << File.join(ROOT, 'lib', 'paperclip')
require File.join(ROOT, 'lib', 'paperclip.rb') require File.join(ROOT, 'lib', 'paperclip.rb')
require 'shoulda_macros/paperclip' require './shoulda_macros/paperclip'
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures") FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
...@@ -61,10 +49,10 @@ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FIL ...@@ -61,10 +49,10 @@ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FIL
ActiveRecord::Base.establish_connection(config['test']) ActiveRecord::Base.establish_connection(config['test'])
def reset_class class_name def reset_class class_name
ActiveRecord::Base.send(:include, Paperclip) ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, class_name) rescue nil Object.send(:remove_const, class_name) rescue nil
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.class_eval{ include Paperclip } klass.class_eval{ include Paperclip::Glue }
klass klass
end end
...@@ -90,11 +78,11 @@ def rebuild_model options = {} ...@@ -90,11 +78,11 @@ def rebuild_model options = {}
end end
def rebuild_class options = {} def rebuild_class options = {}
ActiveRecord::Base.send(:include, Paperclip) ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, "Dummy") rescue nil Object.send(:remove_const, "Dummy") rescue nil
Object.const_set("Dummy", Class.new(ActiveRecord::Base)) Object.const_set("Dummy", Class.new(ActiveRecord::Base))
Dummy.class_eval do Dummy.class_eval do
include Paperclip include Paperclip::Glue
has_attached_file :avatar, options has_attached_file :avatar, options
end end
end end
......
require 'test/helper' require './test/helper'
class IntegrationTest < Test::Unit::TestCase class IntegrationTest < Test::Unit::TestCase
context "Many models at once" do context "Many models at once" do
......
require 'test/helper' require './test/helper'
class InterpolationsTest < Test::Unit::TestCase class InterpolationsTest < Test::Unit::TestCase
should "return all methods but the infrastructure when sent #all" do should "return all methods but the infrastructure when sent #all" do
......
require 'test/helper' require './test/helper'
class IOStreamTest < Test::Unit::TestCase class IOStreamTest < Test::Unit::TestCase
context "IOStream" do include IOStream
should "be included in IO, File, Tempfile, and StringIO" do
[IO, File, Tempfile, StringIO].each do |klass|
assert klass.included_modules.include?(IOStream), "Not in #{klass}"
end
end
end
context "A file" do context "A file" do
setup do setup do
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb') @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
...@@ -21,7 +14,7 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -21,7 +14,7 @@ class IOStreamTest < Test::Unit::TestCase
context "and given a String" do context "and given a String" do
setup do setup do
FileUtils.mkdir_p(File.join(ROOT, 'tmp')) FileUtils.mkdir_p(File.join(ROOT, 'tmp'))
assert @result = @file.stream_to(File.join(ROOT, 'tmp', 'iostream.string.test')) assert @result = stream_to(@file, File.join(ROOT, 'tmp', 'iostream.string.test'))
end end
should "return a File" do should "return a File" do
...@@ -38,7 +31,7 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -38,7 +31,7 @@ class IOStreamTest < Test::Unit::TestCase
setup do setup do
tempfile = Tempfile.new('iostream.test') tempfile = Tempfile.new('iostream.test')
tempfile.binmode tempfile.binmode
assert @result = @file.stream_to(tempfile) assert @result = stream_to(@file, tempfile)
end end
should "return a Tempfile" do should "return a Tempfile" do
...@@ -53,9 +46,9 @@ class IOStreamTest < Test::Unit::TestCase ...@@ -53,9 +46,9 @@ class IOStreamTest < Test::Unit::TestCase
end end
context "that is sent #to_tempfile" do context "that is converted #to_tempfile" do
setup do setup do
assert @tempfile = @file.to_tempfile assert @tempfile = to_tempfile(@file)
end end
should "convert it to a Paperclip Tempfile" do should "convert it to a Paperclip Tempfile" do
......
require 'test/helper' require './test/helper'
class HaveAttachedFileMatcherTest < Test::Unit::TestCase class HaveAttachedFileMatcherTest < Test::Unit::TestCase
context "have_attached_file" do context "have_attached_file" do
......
require 'test/helper' require './test/helper'
class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase class ValidateAttachmentContentTypeMatcherTest < Test::Unit::TestCase
context "validate_attachment_content_type" do context "validate_attachment_content_type" do
......
require 'test/helper' require './test/helper'
class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase class ValidateAttachmentPresenceMatcherTest < Test::Unit::TestCase
context "validate_attachment_presence" do context "validate_attachment_presence" do
......
require 'test/helper' require './test/helper'
class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase class ValidateAttachmentSizeMatcherTest < Test::Unit::TestCase
context "validate_attachment_size" do context "validate_attachment_size" do
......
require 'test/helper' require './test/helper'
class PaperclipTest < Test::Unit::TestCase class PaperclipTest < Test::Unit::TestCase
context "Calling Paperclip.run" do context "Calling Paperclip.run" do
...@@ -43,6 +43,23 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -43,6 +43,23 @@ class PaperclipTest < Test::Unit::TestCase
end end
end end
context "Paperclip.each_instance_with_attachment" do
setup do
@file = File.new(File.join(FIXTURES_DIR, "5k.png"), 'rb')
d1 = Dummy.create(:avatar => @file)
d2 = Dummy.create
d3 = Dummy.create(:avatar => @file)
@expected = [d1, d3]
end
should "yield every instance of a model that has an attachment" do
actual = []
Paperclip.each_instance_with_attachment("Dummy", "avatar") do |instance|
actual << instance
end
assert_same_elements @expected, actual
end
end
should "raise when sent #processor and the name of a class that exists but isn't a subclass of Processor" do 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) } assert_raises(Paperclip::PaperclipError){ Paperclip.processor(:attachment) }
end end
...@@ -172,6 +189,12 @@ class PaperclipTest < Test::Unit::TestCase ...@@ -172,6 +189,12 @@ class PaperclipTest < Test::Unit::TestCase
end end
end end
should "not have Attachment in the ActiveRecord::Base namespace" do
assert_raises(NameError) do
ActiveRecord::Base::Attachment
end
end
def self.should_validate validation, options, valid_file, invalid_file def self.should_validate validation, options, valid_file, invalid_file
context "with #{validation} validation and #{options.inspect} options" do context "with #{validation} validation and #{options.inspect} options" do
setup do setup do
......
require 'test/helper' require './test/helper'
class ProcessorTest < Test::Unit::TestCase class ProcessorTest < Test::Unit::TestCase
should "instantiate and call #make when sent #make to the class" do should "instantiate and call #make when sent #make to the class" do
......
require 'test/helper' require './test/helper'
require 'aws/s3' require 'aws/s3'
class StorageTest < Test::Unit::TestCase class StorageTest < Test::Unit::TestCase
...@@ -185,6 +185,21 @@ class StorageTest < Test::Unit::TestCase ...@@ -185,6 +185,21 @@ class StorageTest < Test::Unit::TestCase
end end
end end
context "and saved without a bucket" do
setup do
class AWS::S3::NoSuchBucket < AWS::S3::ResponseError
# Force the class to be created as a proper subclass of ResponseError thanks to AWS::S3's autocreation of exceptions
end
AWS::S3::Bucket.expects(:create).with("testing")
AWS::S3::S3Object.stubs(:store).raises(AWS::S3::NoSuchBucket.new(:message, :response)).then.returns(true)
@dummy.save
end
should "succeed" do
assert true
end
end
context "and remove" do context "and remove" do
setup do setup do
AWS::S3::S3Object.stubs(:exists?).returns(true) AWS::S3::S3Object.stubs(:exists?).returns(true)
...@@ -336,6 +351,11 @@ class StorageTest < Test::Unit::TestCase ...@@ -336,6 +351,11 @@ class StorageTest < Test::Unit::TestCase
should "be on S3" do should "be on S3" do
assert true assert true
end end
should "generate a tempfile with the right name" do
file = @dummy.avatar.to_file
assert_match /^original.*\.png$/, File.basename(file.path)
end
end end
end end
end end
......
# encoding: utf-8 # encoding: utf-8
require 'test/helper' require './test/helper'
class StyleTest < Test::Unit::TestCase class StyleTest < Test::Unit::TestCase
......
require 'test/helper' require './test/helper'
class ThumbnailTest < Test::Unit::TestCase class ThumbnailTest < Test::Unit::TestCase
context "A Paperclip Tempfile" do context "A Paperclip Tempfile" do
setup do setup do
@tempfile = Paperclip::Tempfile.new("file.jpg") @tempfile = Paperclip::Tempfile.new(["file", ".jpg"])
end end
should "have its path contain a real extension" do should "have its path contain a real extension" do
......
require 'test/helper' require './test/helper'
class UpfileTest < Test::Unit::TestCase class UpfileTest < Test::Unit::TestCase
{ %w(jpg jpe jpeg) => 'image/jpeg', { %w(jpg jpe jpeg) => 'image/jpeg',
......
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