jcnetdev-paperclip 1.0.20080704

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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