jcnetdev-paperclip 1.0.20080704

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,243 @@
1
+ module Paperclip
2
+ # The Attachment class manages the files for a given attachment. It saves when the model saves,
3
+ # deletes when the model is destroyed, and processes the file upon assignment.
4
+ class Attachment
5
+
6
+ def self.default_options
7
+ @default_options ||= {
8
+ :url => "/:attachment/:id/:style/:basename.:extension",
9
+ :path => ":rails_root/public/:attachment/:id/:style/:basename.:extension",
10
+ :styles => {},
11
+ :default_url => "/:attachment/:style/missing.png",
12
+ :default_style => :original,
13
+ :validations => [],
14
+ :storage => :filesystem
15
+ }
16
+ end
17
+
18
+ attr_reader :name, :instance, :styles, :default_style
19
+
20
+ # Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
21
+ # ActiveRecord object instance it's attached to, and +options+ is the same as the hash
22
+ # passed to +has_attached_file+.
23
+ def initialize name, instance, options = {}
24
+ @name = name
25
+ @instance = instance
26
+
27
+ options = self.class.default_options.merge(options)
28
+
29
+ @url = options[:url]
30
+ @path = options[:path]
31
+ @styles = options[:styles]
32
+ @default_url = options[:default_url]
33
+ @validations = options[:validations]
34
+ @default_style = options[:default_style]
35
+ @storage = options[:storage]
36
+ @whiny_thumbnails = options[:whiny_thumbnails]
37
+ @options = options
38
+ @queued_for_delete = []
39
+ @queued_for_write = {}
40
+ @errors = []
41
+ @validation_errors = nil
42
+ @dirty = false
43
+
44
+ normalize_style_definition
45
+ initialize_storage
46
+ end
47
+
48
+ # What gets called when you call instance.attachment = File. It clears errors,
49
+ # assigns attributes, processes the file, and runs validations. It also queues up
50
+ # the previous file for deletion, to be flushed away on #save of its host.
51
+ def assign uploaded_file
52
+ return nil unless valid_assignment?(uploaded_file)
53
+
54
+ queue_existing_for_delete
55
+ @errors = []
56
+ @validation_errors = nil
57
+
58
+ return nil if uploaded_file.nil?
59
+
60
+ @queued_for_write[:original] = uploaded_file.to_tempfile
61
+ @instance[:"#{@name}_file_name"] = uploaded_file.original_filename.strip.gsub /[^\w\d\.\-]+/, '_'
62
+ @instance[:"#{@name}_content_type"] = uploaded_file.content_type.strip
63
+ @instance[:"#{@name}_file_size"] = uploaded_file.size.to_i
64
+
65
+ @dirty = true
66
+
67
+ post_process
68
+ ensure
69
+ validate
70
+ end
71
+
72
+ # Returns the public URL of the attachment, with a given style. Note that this
73
+ # does not necessarily need to point to a file that your web server can access
74
+ # and can point to an action in your app, if you need fine grained security.
75
+ # This is not recommended if you don't need the security, however, for
76
+ # performance reasons.
77
+ def url style = default_style
78
+ original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
79
+ end
80
+
81
+ # Returns the path of the attachment as defined by the :path optionn. If the
82
+ # file is stored in the filesystem the path refers to the path of the file on
83
+ # disk. If the file is stored in S3, the path is the "key" part of th URL,
84
+ # and the :bucket option refers to the S3 bucket.
85
+ def path style = nil #:nodoc:
86
+ interpolate(@path, style)
87
+ end
88
+
89
+ # Alias to +url+
90
+ def to_s style = nil
91
+ url(style)
92
+ end
93
+
94
+ # Returns true if there are any errors on this attachment.
95
+ def valid?
96
+ errors.length == 0
97
+ end
98
+
99
+ # Returns an array containing the errors on this attachment.
100
+ def errors
101
+ @errors.compact.uniq
102
+ end
103
+
104
+ # Returns true if there are changes that need to be saved.
105
+ def dirty?
106
+ @dirty
107
+ end
108
+
109
+ # Saves the file, if there are no errors. If there are, it flushes them to
110
+ # the instance's errors and returns false, cancelling the save.
111
+ def save
112
+ if valid?
113
+ flush_deletes
114
+ flush_writes
115
+ @dirty = false
116
+ true
117
+ else
118
+ flush_errors
119
+ false
120
+ end
121
+ end
122
+
123
+ # Returns the name of the file as originally assigned, and as lives in the
124
+ # <attachment>_file_name attribute of the model.
125
+ def original_filename
126
+ instance[:"#{name}_file_name"]
127
+ end
128
+
129
+ # A hash of procs that are run during the interpolation of a path or url.
130
+ # A variable of the format :name will be replaced with the return value of
131
+ # the proc named ":name". Each lambda takes the attachment and the current
132
+ # style as arguments. This hash can be added to with your own proc if
133
+ # necessary.
134
+ def self.interpolations
135
+ @interpolations ||= {
136
+ :rails_root => lambda{|attachment,style| RAILS_ROOT },
137
+ :class => lambda do |attachment,style|
138
+ attachment.instance.class.name.underscore.pluralize
139
+ end,
140
+ :basename => lambda do |attachment,style|
141
+ attachment.original_filename.gsub(File.extname(attachment.original_filename), "")
142
+ end,
143
+ :extension => lambda do |attachment,style|
144
+ ((style = attachment.styles[style]) && style.last) ||
145
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
146
+ end,
147
+ :id => lambda{|attachment,style| attachment.instance.id },
148
+ :id_partition => lambda do |attachment, style|
149
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
150
+ end,
151
+ :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
152
+ :style => lambda{|attachment,style| style || attachment.default_style },
153
+ }
154
+ end
155
+
156
+ # This method really shouldn't be called that often. It's expected use is in the
157
+ # paperclip:refresh rake task and that's it. It will regenerate all thumbnails
158
+ # forcefully, by reobtaining the original file and going through the post-process
159
+ # again.
160
+ def reprocess!
161
+ new_original = Tempfile.new("paperclip-reprocess")
162
+ old_original = to_file(:original)
163
+ new_original.write( old_original.read )
164
+ new_original.rewind
165
+
166
+ @queued_for_write = { :original => new_original }
167
+ post_process
168
+
169
+ old_original.close if old_original.respond_to?(:close)
170
+ end
171
+
172
+ private
173
+
174
+ def valid_assignment? file #:nodoc:
175
+ file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
176
+ end
177
+
178
+ def validate #:nodoc:
179
+ unless @validation_errors
180
+ @validation_errors = @validations.collect do |v|
181
+ v.call(self, instance)
182
+ end.flatten.compact.uniq
183
+ @errors += @validation_errors
184
+ end
185
+ end
186
+
187
+ def normalize_style_definition
188
+ @styles.each do |name, args|
189
+ dimensions, format = [args, nil].flatten[0..1]
190
+ format = nil if format == ""
191
+ @styles[name] = [dimensions, format]
192
+ end
193
+ end
194
+
195
+ def initialize_storage
196
+ @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
197
+ self.extend(@storage_module)
198
+ end
199
+
200
+ def post_process #:nodoc:
201
+ return if @queued_for_write[:original].nil?
202
+ @styles.each do |name, args|
203
+ begin
204
+ dimensions, format = args
205
+ @queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
206
+ dimensions,
207
+ format,
208
+ @whiny_thumnails)
209
+ rescue PaperclipError => e
210
+ @errors << e.message if @whiny_thumbnails
211
+ end
212
+ end
213
+ end
214
+
215
+ def interpolate pattern, style = default_style #:nodoc:
216
+ interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
217
+ interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
218
+ tag, blk = interpolation
219
+ result.gsub(/:#{tag}/) do |match|
220
+ blk.call( self, style )
221
+ end
222
+ end
223
+ end
224
+
225
+ def queue_existing_for_delete #:nodoc:
226
+ return if original_filename.blank?
227
+ @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
228
+ path(style) if exists?(style)
229
+ end.compact
230
+ @instance[:"#{@name}_file_name"] = nil
231
+ @instance[:"#{@name}_content_type"] = nil
232
+ @instance[:"#{@name}_file_size"] = nil
233
+ end
234
+
235
+ def flush_errors #:nodoc:
236
+ @errors.each do |error|
237
+ instance.errors.add(name, error)
238
+ end
239
+ end
240
+
241
+ end
242
+ end
243
+
@@ -0,0 +1,109 @@
1
+ module Paperclip
2
+
3
+ # Defines the geometry of an image.
4
+ class Geometry
5
+ attr_accessor :height, :width, :modifier
6
+
7
+ # Gives a Geometry representing the given height and width
8
+ def initialize width = nil, height = nil, modifier = nil
9
+ height = nil if height == ""
10
+ width = nil if width == ""
11
+ @height = (height || width).to_f
12
+ @width = (width || height).to_f
13
+ @modifier = modifier
14
+ end
15
+
16
+ # Uses ImageMagick to determing the dimensions of a file, passed in as either a
17
+ # File or path.
18
+ def self.from_file file
19
+ file = file.path if file.respond_to? "path"
20
+ parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
21
+ raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
22
+ end
23
+
24
+ # Parses a "WxH" formatted string, where W is the width and H is the height.
25
+ def self.parse string
26
+ if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
27
+ Geometry.new(*match[1,3])
28
+ end
29
+ end
30
+
31
+ # True if the dimensions represent a square
32
+ def square?
33
+ height == width
34
+ end
35
+
36
+ # True if the dimensions represent a horizontal rectangle
37
+ def horizontal?
38
+ height < width
39
+ end
40
+
41
+ # True if the dimensions represent a vertical rectangle
42
+ def vertical?
43
+ height > width
44
+ end
45
+
46
+ # The aspect ratio of the dimensions.
47
+ def aspect
48
+ width / height
49
+ end
50
+
51
+ # Returns the larger of the two dimensions
52
+ def larger
53
+ [height, width].max
54
+ end
55
+
56
+ # Returns the smaller of the two dimensions
57
+ def smaller
58
+ [height, width].min
59
+ end
60
+
61
+ # Returns the width and height in a format suitable to be passed to Geometry.parse
62
+ def to_s
63
+ "%dx%d%s" % [width, height, modifier]
64
+ end
65
+
66
+ # Same as to_s
67
+ def inspect
68
+ to_s
69
+ end
70
+
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
77
+ # is weighted at the center of the Geometry.
78
+ def transformation_to dst, crop = false
79
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
80
+
81
+ if crop
82
+ scale_geometry, scale = scaling(dst, ratio)
83
+ crop_geometry = cropping(dst, ratio, scale)
84
+ else
85
+ scale_geometry = dst.to_s
86
+ end
87
+
88
+ [ scale_geometry, crop_geometry ]
89
+ end
90
+
91
+ private
92
+
93
+ def scaling dst, ratio
94
+ if ratio.horizontal? || ratio.square?
95
+ [ "%dx" % dst.width, ratio.width ]
96
+ else
97
+ [ "x%d" % dst.height, ratio.height ]
98
+ end
99
+ end
100
+
101
+ def cropping dst, ratio, scale
102
+ if ratio.horizontal? || ratio.square?
103
+ "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
104
+ else
105
+ "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,43 @@
1
+ # Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
2
+ # and Tempfile conversion.
3
+ module IOStream
4
+
5
+ # Returns a Tempfile containing the contents of the readable object.
6
+ def to_tempfile
7
+ tempfile = Tempfile.new("stream")
8
+ tempfile.binmode
9
+ self.stream_to(tempfile)
10
+ end
11
+
12
+ # Copies one read-able object from one place to another in blocks, obviating the need to load
13
+ # the whole thing into memory. Defaults to 8k blocks. If this module is included in both
14
+ # both StringIO and Tempfile, then either can have its data copied anywhere else without typing
15
+ # worries or memory overhead worries. Returns a File if a String is passed in as the destination
16
+ # and returns the IO or Tempfile as passed in if one is sent as the destination.
17
+ def stream_to path_or_file, in_blocks_of = 8192
18
+ dstio = case path_or_file
19
+ when String then File.new(path_or_file, "wb+")
20
+ when IO then path_or_file
21
+ when Tempfile then path_or_file
22
+ end
23
+ buffer = ""
24
+ self.rewind
25
+ while self.read(in_blocks_of, buffer) do
26
+ dstio.write(buffer)
27
+ end
28
+ dstio.rewind
29
+ dstio
30
+ end
31
+ end
32
+
33
+ class IO
34
+ include IOStream
35
+ end
36
+
37
+ %w( Tempfile StringIO ).each do |klass|
38
+ if Object.const_defined? klass
39
+ Object.const_get(klass).class_eval do
40
+ include IOStream
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,179 @@
1
+ module Paperclip
2
+ module Storage
3
+
4
+ # The default place to store attachments is in the filesystem. Files on the local
5
+ # filesystem can be very easily served by Apache without requiring a hit to your app.
6
+ # They also can be processed more easily after they've been saved, as they're just
7
+ # normal files. There is one Filesystem-specific option for has_attached_file.
8
+ # * +path+: The location of the repository of attachments on disk. This can (and, in
9
+ # almost all cases, should) be coordinated with the value of the +url+ option to
10
+ # allow files to be saved into a place where Apache can serve them without
11
+ # hitting your app. Defaults to
12
+ # ":rails_root/public/:class/:attachment/:id/:style_:filename".
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
+ # public directory.
17
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
+ # :path => "/var/app/attachments/:class/:id/:style/:filename"
19
+ module Filesystem
20
+ def self.extended base
21
+ end
22
+
23
+ def exists?(style = default_style)
24
+ if original_filename
25
+ File.exist?(path(style))
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ # Returns representation of the data of the file assigned to the given
32
+ # style, in the format most representative of the current storage.
33
+ def to_file style = default_style
34
+ @queued_for_write[style] || (File.new(path(style)) if exists?(style))
35
+ end
36
+ alias_method :to_io, :to_file
37
+
38
+ def flush_writes #:nodoc:
39
+ @queued_for_write.each do |style, file|
40
+ FileUtils.mkdir_p(File.dirname(path(style)))
41
+ result = file.stream_to(path(style))
42
+ file.close
43
+ result.close
44
+ end
45
+ @queued_for_write = {}
46
+ end
47
+
48
+ def flush_deletes #:nodoc:
49
+ @queued_for_delete.each do |path|
50
+ begin
51
+ FileUtils.rm(path) if File.exist?(path)
52
+ rescue Errno::ENOENT => e
53
+ # ignore file-not-found, let everything else pass
54
+ end
55
+ end
56
+ @queued_for_delete = []
57
+ end
58
+ end
59
+
60
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
61
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
62
+ # There are a few S3-specific options for has_attached_file:
63
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
64
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
65
+ # gives you. You can 'environment-space' this just like you do to your
66
+ # database.yml file, so different environments can use different accounts:
67
+ # development:
68
+ # access_key_id: 123...
69
+ # secret_access_key: 123...
70
+ # test:
71
+ # access_key_id: abc...
72
+ # secret_access_key: abc...
73
+ # production:
74
+ # access_key_id: 456...
75
+ # secret_access_key: 456...
76
+ # This is not required, however, and the file may simply look like this:
77
+ # access_key_id: 456...
78
+ # secret_access_key: 456...
79
+ # In which case, those access keys will be used in all environments.
80
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
81
+ # policies that S3 provides (more information can be found here:
82
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
83
+ # The default for Paperclip is "public-read".
84
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
85
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
86
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
87
+ # * +path+: This is the key under the bucket in which the file will be stored. The
88
+ # URL will be constructed from the bucket and the path. This is what you will want
89
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
90
+ # S3 (strictly speaking) does not support directories, you can still use a / to
91
+ # separate parts of your file name.
92
+ module S3
93
+ def self.extended base
94
+ require 'right_aws'
95
+ base.instance_eval do
96
+ @bucket = @options[:bucket]
97
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
98
+ @s3_options = @options[:s3_options] || {}
99
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
100
+ @url = ":s3_url"
101
+ end
102
+ base.class.interpolations[:s3_url] = lambda do |attachment, style|
103
+ "https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
104
+ end
105
+ end
106
+
107
+ def s3
108
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
109
+ @s3_credentials[:secret_access_key],
110
+ @s3_options)
111
+ end
112
+
113
+ def s3_bucket
114
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
115
+ end
116
+
117
+ def bucket_name
118
+ @bucket
119
+ end
120
+
121
+ def parse_credentials creds
122
+ creds = find_credentials(creds).stringify_keys
123
+ (creds[ENV['RAILS_ENV']] || creds).symbolize_keys
124
+ end
125
+
126
+ def exists?(style = default_style)
127
+ s3_bucket.key(path(style)) ? true : false
128
+ end
129
+
130
+ # Returns representation of the data of the file assigned to the given
131
+ # style, in the format most representative of the current storage.
132
+ def to_file style = default_style
133
+ @queued_for_write[style] || s3_bucket.key(path(style))
134
+ end
135
+ alias_method :to_io, :to_file
136
+
137
+ def flush_writes #:nodoc:
138
+ @queued_for_write.each do |style, file|
139
+ begin
140
+ key = s3_bucket.key(path(style))
141
+ key.data = file
142
+ key.put(nil, @s3_permissions)
143
+ rescue RightAws::AwsError => e
144
+ raise
145
+ end
146
+ end
147
+ @queued_for_write = {}
148
+ end
149
+
150
+ def flush_deletes #:nodoc:
151
+ @queued_for_delete.each do |path|
152
+ begin
153
+ if file = s3_bucket.key(path)
154
+ file.delete
155
+ end
156
+ rescue RightAws::AwsError
157
+ # Ignore this.
158
+ end
159
+ end
160
+ @queued_for_delete = []
161
+ end
162
+
163
+ def find_credentials creds
164
+ case creds
165
+ when File:
166
+ YAML.load_file(creds.path)
167
+ when String:
168
+ YAML.load_file(creds)
169
+ when Hash:
170
+ creds
171
+ else
172
+ raise ArgumentError, "Credentials are not a path, file, or hash."
173
+ end
174
+ end
175
+ private :find_credentials
176
+
177
+ end
178
+ end
179
+ end