dm-paperclip 2.1.4 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ module Paperclip
2
+ # This module is intended as a compatability shim for the differences in
3
+ # callbacks between Rails 2.0 and Rails 2.1.
4
+ module CallbackCompatability
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.send(:include, InstanceMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ # The implementation of this method is taken from the Rails 1.2.6 source,
12
+ # from rails/activerecord/lib/active_record/callbacks.rb, line 192.
13
+ def define_callbacks(*args)
14
+ args.each do |method|
15
+ self.class_eval <<-"end_eval"
16
+ def self.#{method}(*callbacks, &block)
17
+ callbacks << block if block_given?
18
+ write_inheritable_array(#{method.to_sym.inspect}, callbacks)
19
+ end
20
+ end_eval
21
+ end
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ # The callbacks in < 2.1 don't worry about the extra options or the
27
+ # block, so just run what we have available.
28
+ def run_callbacks(meth, opts = nil, &blk)
29
+ callback(meth)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,13 +1,13 @@
1
1
  module Paperclip
2
-
2
+
3
3
  # Defines the geometry of an image.
4
4
  class Geometry
5
5
  attr_accessor :height, :width, :modifier
6
6
 
7
7
  # Gives a Geometry representing the given height and width
8
8
  def initialize width = nil, height = nil, modifier = nil
9
- height = nil if height == ""
10
- width = nil if width == ""
9
+ height = nil if height == ''
10
+ width = nil if width == ''
11
11
  @height = (height || width).to_f
12
12
  @width = (width || height).to_f
13
13
  @modifier = modifier
@@ -17,13 +17,18 @@ module Paperclip
17
17
  # File or path.
18
18
  def self.from_file file
19
19
  file = file.path if file.respond_to? "path"
20
- parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
20
+ geometry = begin
21
+ Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]])
22
+ rescue PaperclipCommandLineError
23
+ ""
24
+ end
25
+ parse(geometry) ||
21
26
  raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
22
27
  end
23
28
 
24
29
  # Parses a "WxH" formatted string, where W is the width and H is the height.
25
30
  def self.parse string
26
- if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
31
+ if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
27
32
  Geometry.new(*match[1,3])
28
33
  end
29
34
  end
@@ -60,7 +65,11 @@ module Paperclip
60
65
 
61
66
  # Returns the width and height in a format suitable to be passed to Geometry.parse
62
67
  def to_s
63
- "%dx%d%s" % [width, height, modifier]
68
+ s = ""
69
+ s << width.to_i.to_s if width > 0
70
+ s << "x#{height.to_i}" if height > 0
71
+ s << modifier.to_s
72
+ s
64
73
  end
65
74
 
66
75
  # Same as to_s
@@ -68,15 +77,14 @@ module Paperclip
68
77
  to_s
69
78
  end
70
79
 
71
- # Returns the scaling and cropping geometries (in string-based ImageMagick format)
72
- # neccessary to transform this Geometry into the Geometry given. If crop is true,
73
- # then it is assumed the destination Geometry will be the exact final resolution.
74
- # In this case, the source Geometry is scaled so that an image containing the
75
- # destination Geometry would be completely filled by the source image, and any
76
- # overhanging image would be cropped. Useful for square thumbnail images. The cropping
80
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
81
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
82
+ # then it is assumed the destination Geometry will be the exact final resolution.
83
+ # In this case, the source Geometry is scaled so that an image containing the
84
+ # destination Geometry would be completely filled by the source image, and any
85
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
77
86
  # is weighted at the center of the Geometry.
78
87
  def transformation_to dst, crop = false
79
-
80
88
  if crop
81
89
  ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
82
90
  scale_geometry, scale = scaling(dst, ratio)
@@ -84,7 +92,7 @@ module Paperclip
84
92
  else
85
93
  scale_geometry = dst.to_s
86
94
  end
87
-
95
+
88
96
  [ scale_geometry, crop_geometry ]
89
97
  end
90
98
 
@@ -0,0 +1,123 @@
1
+ module Paperclip
2
+ # This module contains all the methods that are available for interpolation
3
+ # in paths and urls. To add your own (or override an existing one), you
4
+ # can either open this module and define it, or call the
5
+ # Paperclip.interpolates method.
6
+ module Interpolations
7
+ extend self
8
+
9
+ # Hash assignment of interpolations. Included only for compatability,
10
+ # and is not intended for normal use.
11
+ def self.[]= name, block
12
+ define_method(name, &block)
13
+ end
14
+
15
+ # Hash access of interpolations. Included only for compatability,
16
+ # and is not intended for normal use.
17
+ def self.[] name
18
+ method(name)
19
+ end
20
+
21
+ # Returns a sorted list of all interpolations.
22
+ def self.all
23
+ self.instance_methods(false).sort
24
+ end
25
+
26
+ # Perform the actual interpolation. Takes the pattern to interpolate
27
+ # and the arguments to pass, which are the attachment and style name.
28
+ def self.interpolate pattern, *args
29
+ all.reverse.inject( pattern.dup ) do |result, tag|
30
+ result.gsub(/:#{tag}/) do |match|
31
+ send( tag, *args )
32
+ end
33
+ end
34
+ end
35
+
36
+ # Returns the filename, the same way as ":basename.:extension" would.
37
+ def filename attachment, style
38
+ "#{basename(attachment, style)}.#{extension(attachment, style)}"
39
+ end
40
+
41
+ # Returns the interpolated URL. Will raise an error if the url itself
42
+ # contains ":url" to prevent infinite recursion. This interpolation
43
+ # is used in the default :path to ease default specifications.
44
+ def url attachment, style
45
+ raise InfiniteInterpolationError if attachment.options[:url].include?(":url")
46
+ attachment.url(style, false)
47
+ end
48
+
49
+ # Returns the timestamp as defined by the <attachment>_updated_at field
50
+ def timestamp attachment, style
51
+ attachment.instance_read(:updated_at).to_s
52
+ end
53
+
54
+ def web_root attachment, style
55
+ if Object.const_defined?('Merb')
56
+ merb_root(attachment, style)
57
+ elsif Object.const_defined("RAILS_ROOT")
58
+ rails_root(attachment, style)
59
+ else
60
+ ""
61
+ end
62
+ end
63
+
64
+ # Returns the RAILS_ROOT constant.
65
+ def rails_root attachment, style
66
+ Object.const_defined?('RAILS_ROOT') ? RAILS_ROOT : nil
67
+ end
68
+
69
+ # Returns the RAILS_ENV constant.
70
+ def rails_env attachment, style
71
+ Object.const_defined?('RAILS_ENV') ? RAILS_ENV : nil
72
+ end
73
+
74
+ def merb_root attachment, style
75
+ Object.const_defined?('Merb') ? Merb.root : nil
76
+ end
77
+
78
+ def merb_env attachment, style
79
+ Object.const_defined?('Merb') ? Merb.env : nil
80
+ end
81
+
82
+ # Returns the snake cased, pluralized version of the class name.
83
+ # e.g. "users" for the User class.
84
+ def class attachment, style
85
+ attachment.instance.class.to_s.snake_case.pluralize
86
+ end
87
+
88
+ # Returns the basename of the file. e.g. "file" for "file.jpg"
89
+ def basename attachment, style
90
+ attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "")
91
+ end
92
+
93
+ # Returns the extension of the file. e.g. "jpg" for "file.jpg"
94
+ # If the style has a format defined, it will return the format instead
95
+ # of the actual extension.
96
+ def extension attachment, style
97
+ ((style = attachment.styles[style]) && style[:format]) ||
98
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
99
+ end
100
+
101
+ # Returns the id of the instance.
102
+ def id attachment, style
103
+ attachment.instance.id
104
+ end
105
+
106
+ # Returns the id of the instance in a split path form. e.g. returns
107
+ # 000/001/234 for an id of 1234.
108
+ def id_partition attachment, style
109
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
110
+ end
111
+
112
+ # Returns the pluralized form of the attachment name. e.g.
113
+ # "avatars" for an attachment of :avatar
114
+ def attachment attachment, style
115
+ attachment.name.to_s.downcase.pluralize
116
+ end
117
+
118
+ # Returns the style, or the default style if nil is supplied.
119
+ def style attachment, style
120
+ style || attachment.default_style
121
+ end
122
+ end
123
+ end
@@ -25,12 +25,12 @@ module IOStream
25
25
  while self.read(in_blocks_of, buffer) do
26
26
  dstio.write(buffer)
27
27
  end
28
- dstio.rewind
28
+ dstio.rewind
29
29
  dstio
30
30
  end
31
31
  end
32
32
 
33
- class IO
33
+ class IO #:nodoc:
34
34
  include IOStream
35
35
  end
36
36
 
@@ -41,3 +41,18 @@ end
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ # Corrects a bug in Windows when asking for Tempfile size.
46
+ if defined? Tempfile
47
+ class Tempfile
48
+ def size
49
+ if @tmpfile
50
+ @tmpfile.fsync
51
+ @tmpfile.flush
52
+ @tmpfile.stat.size
53
+ else
54
+ 0
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ module Paperclip
2
+ # Paperclip processors allow you to modify attached files when they are
3
+ # attached in any way you are able. Paperclip itself uses command-line
4
+ # programs for its included Thumbnail processor, but custom processors
5
+ # are not required to follow suit.
6
+ #
7
+ # Processors are required to be defined inside the Paperclip module and
8
+ # are also required to be a subclass of Paperclip::Processor. There is
9
+ # only one method you *must* implement to properly be a subclass:
10
+ # #make, but #initialize may also be of use. Both methods accept 3
11
+ # arguments: the file that will be operated on (which is an instance of
12
+ # File), a hash of options that were defined in has_attached_file's
13
+ # style hash, and the Paperclip::Attachment itself.
14
+ #
15
+ # All #make needs to return is an instance of File (Tempfile is
16
+ # acceptable) which contains the results of the processing.
17
+ #
18
+ # See Paperclip.run for more information about using command-line
19
+ # utilities from within Processors.
20
+ class Processor
21
+ attr_accessor :file, :options, :attachment
22
+
23
+ def initialize file, options = {}, attachment = nil
24
+ @file = file
25
+ @options = options
26
+ @attachment = attachment
27
+ end
28
+
29
+ def make
30
+ end
31
+
32
+ def self.make file, options = {}, attachment = nil
33
+ new(file, options, attachment).make
34
+ end
35
+ end
36
+
37
+ # Due to how ImageMagick handles its image format conversion and how Tempfile
38
+ # handles its naming scheme, it is necessary to override how Tempfile makes
39
+ # its names so as to allow for file extensions. Idea taken from the comments
40
+ # on this blog post:
41
+ # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
42
+ class Tempfile < ::Tempfile
43
+ # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
44
+ def make_tmpname(basename, n)
45
+ extension = File.extname(basename)
46
+ sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
47
+ end
48
+ end
49
+ end
@@ -8,18 +8,18 @@ module Paperclip
8
8
  # * +path+: The location of the repository of attachments on disk. This can (and, in
9
9
  # almost all cases, should) be coordinated with the value of the +url+ option to
10
10
  # allow files to be saved into a place where Apache can serve them without
11
- # hitting your app. Defaults to
11
+ # hitting your app. Defaults to
12
12
  # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13
- # By default this places the files in the app's public directory which can be served
14
- # directly. If you are using capistrano for deployment, a good idea would be to
15
- # make a symlink to the capistrano-created system directory from inside your app's
13
+ # By default this places the files in the app's public directory which can be served
14
+ # directly. If you are using capistrano for deployment, a good idea would be to
15
+ # make a symlink to the capistrano-created system directory from inside your app's
16
16
  # public directory.
17
17
  # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
- # :path => "/var/app/attachments/:class/:id/:style/:filename"
18
+ # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19
19
  module Filesystem
20
20
  def self.extended base
21
21
  end
22
-
22
+
23
23
  def exists?(style = default_style)
24
24
  if original_filename
25
25
  File.exist?(path(style))
@@ -31,31 +31,40 @@ module Paperclip
31
31
  # Returns representation of the data of the file assigned to the given
32
32
  # style, in the format most representative of the current storage.
33
33
  def to_file style = default_style
34
- @queued_for_write[style] || (File.new(path(style)) if exists?(style))
34
+ @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
35
35
  end
36
36
  alias_method :to_io, :to_file
37
37
 
38
38
  def flush_writes #:nodoc:
39
- #logger.info("[paperclip] Writing files for #{name}")
40
39
  @queued_for_write.each do |style, file|
41
- FileUtils.mkdir_p(File.dirname(path(style)))
42
- #logger.info("[paperclip] -> #{path(style)}")
43
- result = file.stream_to(path(style))
44
40
  file.close
45
- result.close
41
+ FileUtils.mkdir_p(File.dirname(path(style)))
42
+ log("saving #{path(style)}")
43
+ FileUtils.mv(file.path, path(style))
44
+ FileUtils.chmod(0644, path(style))
46
45
  end
47
46
  @queued_for_write = {}
48
47
  end
49
48
 
50
49
  def flush_deletes #:nodoc:
51
- #logger.info("[paperclip] Deleting files for #{name}")
52
50
  @queued_for_delete.each do |path|
53
51
  begin
54
- #logger.info("[paperclip] -> #{path}")
52
+ log("deleting #{path}")
55
53
  FileUtils.rm(path) if File.exist?(path)
56
54
  rescue Errno::ENOENT => e
57
55
  # ignore file-not-found, let everything else pass
58
56
  end
57
+ begin
58
+ while(true)
59
+ path = File.dirname(path)
60
+ FileUtils.rmdir(path)
61
+ end
62
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
63
+ # Stop trying to remove parent directories
64
+ rescue SystemCallError => e
65
+ log("There was an unexpected error while deleting directories: #{e.class}")
66
+ # Ignore it
67
+ end
59
68
  end
60
69
  @queued_for_delete = []
61
70
  end
@@ -70,35 +79,48 @@ module Paperclip
70
79
  # database.yml file, so different environments can use different accounts:
71
80
  # development:
72
81
  # access_key_id: 123...
73
- # secret_access_key: 123...
82
+ # secret_access_key: 123...
74
83
  # test:
75
84
  # access_key_id: abc...
76
- # secret_access_key: abc...
85
+ # secret_access_key: abc...
77
86
  # production:
78
87
  # access_key_id: 456...
79
- # secret_access_key: 456...
88
+ # secret_access_key: 456...
80
89
  # This is not required, however, and the file may simply look like this:
81
90
  # access_key_id: 456...
82
- # secret_access_key: 456...
91
+ # secret_access_key: 456...
83
92
  # In which case, those access keys will be used in all environments. You can also
84
93
  # put your bucket name in this file, instead of adding it to the code directly.
85
- # This is useful when you want the same account but a different bucket for
94
+ # This is useful when you want the same account but a different bucket for
86
95
  # development versus production.
87
96
  # * +s3_permissions+: This is a String that should be one of the "canned" access
88
97
  # policies that S3 provides (more information can be found here:
89
98
  # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
90
99
  # The default for Paperclip is "public-read".
91
- # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
100
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
92
101
  # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
93
102
  # default), and 'https' when your :s3_permissions are anything else.
103
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
94
104
  # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
95
105
  # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
96
106
  # Paperclip will attempt to create it. The bucket name will not be interpolated.
97
- # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
107
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
108
+ # Paperclip will call that Proc with attachment as the only argument.
109
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
110
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
111
+ # link in the +url+ entry for more information about S3 domains and buckets.
112
+ # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
98
113
  # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
114
+ # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
115
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
116
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
99
117
  # Normally, this won't matter in the slightest and you can leave the default (which is
100
118
  # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
101
119
  # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
120
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
121
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
122
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
123
+ # by S3.
102
124
  # * +path+: This is the key under the bucket in which the file will be stored. The
103
125
  # URL will be constructed from the bucket and the path. This is what you will want
104
126
  # to interpolate. Keys should be unique, like filenames, and despite the fact that
@@ -109,19 +131,24 @@ module Paperclip
109
131
  require 'right_aws'
110
132
  base.instance_eval do
111
133
  @s3_credentials = parse_credentials(@options[:s3_credentials])
112
- @bucket = @options[:bucket] || @s3_credentials[:bucket]
113
- @s3_options = @options[:s3_options] || {}
134
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
135
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
136
+ @s3_options = @options[:s3_options] || {}
114
137
  @s3_permissions = @options[:s3_permissions] || 'public-read'
115
- @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
138
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
139
+ @s3_headers = @options[:s3_headers] || {}
140
+ @s3_host_alias = @options[:s3_host_alias]
116
141
  @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
117
142
  end
118
- base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
143
+ Paperclip.interpolates(:s3_alias_url) do |attachment, style|
144
+ "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
145
+ end
146
+ Paperclip.interpolates(:s3_path_url) do |attachment, style|
119
147
  "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
120
148
  end
121
- base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
149
+ Paperclip.interpolates(:s3_domain_url) do |attachment, style|
122
150
  "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
123
151
  end
124
- #ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
125
152
  end
126
153
 
127
154
  def s3
@@ -138,11 +165,19 @@ module Paperclip
138
165
  @bucket
139
166
  end
140
167
 
168
+ def s3_host_alias
169
+ @s3_host_alias
170
+ end
171
+
141
172
  def parse_credentials creds
142
- creds = stringify_keys(find_credentials(creds))
143
- symbolize_keys((creds[Merb.env] || creds))
173
+ creds = find_credentials(creds).to_mash
174
+ if defined? Merb && Merb.respond_to?(:env)
175
+ (creds[Merb.env] || creds)
176
+ else
177
+ (creds[RAILS_ENV] || creds)
178
+ end
144
179
  end
145
-
180
+
146
181
  def exists?(style = default_style)
147
182
  s3_bucket.key(path(style)) ? true : false
148
183
  end
@@ -159,13 +194,12 @@ module Paperclip
159
194
  alias_method :to_io, :to_file
160
195
 
161
196
  def flush_writes #:nodoc:
162
- #logger.info("[paperclip] Writing files for #{name}")
163
197
  @queued_for_write.each do |style, file|
164
198
  begin
165
- #logger.info("[paperclip] -> #{path(style)}")
199
+ log("saving #{path(style)}")
166
200
  key = s3_bucket.key(path(style))
167
201
  key.data = file
168
- key.put(nil, @s3_permissions)
202
+ key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
169
203
  rescue RightAws::AwsError => e
170
204
  raise
171
205
  end
@@ -174,10 +208,9 @@ module Paperclip
174
208
  end
175
209
 
176
210
  def flush_deletes #:nodoc:
177
- #logger.info("[paperclip] Writing files for #{name}")
178
211
  @queued_for_delete.each do |path|
179
212
  begin
180
- #logger.info("[paperclip] -> #{path}")
213
+ log("deleting #{path}")
181
214
  if file = s3_bucket.key(path)
182
215
  file.delete
183
216
  end
@@ -187,14 +220,14 @@ module Paperclip
187
220
  end
188
221
  @queued_for_delete = []
189
222
  end
190
-
223
+
191
224
  def find_credentials creds
192
225
  case creds
193
- when File:
226
+ when File
194
227
  YAML.load_file(creds.path)
195
- when String:
228
+ when String
196
229
  YAML.load_file(creds)
197
- when Hash:
230
+ when Hash
198
231
  creds
199
232
  else
200
233
  raise ArgumentError, "Credentials are not a path, file, or hash."
@@ -202,22 +235,6 @@ module Paperclip
202
235
  end
203
236
  private :find_credentials
204
237
 
205
- private
206
-
207
- def stringify_keys(hash)
208
- hash.inject({}) do |options, (key, value)|
209
- options[key.to_s] = value
210
- options
211
- end
212
- end
213
-
214
- def symbolize_keys(hash)
215
- hash.inject({}) do |options, (key, value)|
216
- options[key.to_sym || key] = value
217
- options
218
- end
219
- end
220
-
221
238
  end
222
239
  end
223
240
  end