jeremydurham-merb_paperclip 0.9.12

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2008 Jon Yurek and thoughtbot, inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,4 @@
1
+ merb_paperclip
2
+ ==============
3
+
4
+ A Merb plugin that is essentially a port of Jon Yurek's paperclip. For Merb + ActiveRecord only.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+
4
+ require 'merb-core'
5
+ require 'merb-core/tasks/merb'
6
+
7
+ GEM_NAME = "merb_paperclip"
8
+ GEM_VERSION = "0.9.12"
9
+ AUTHOR = "Jeremy Durham"
10
+ EMAIL = "jeremydurham@gmail.com"
11
+ HOMEPAGE = "http://www.thoughtbot.com/projects/paperclip/"
12
+ SUMMARY = "A Merb plugin that is essentially a port of Jon Yurek's paperclip"
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.rubyforge_project = 'merb'
16
+ s.name = GEM_NAME
17
+ s.version = GEM_VERSION
18
+ s.platform = Gem::Platform::RUBY
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["README", "LICENSE", 'TODO']
21
+ s.summary = SUMMARY
22
+ s.description = s.summary
23
+ s.author = AUTHOR
24
+ s.email = EMAIL
25
+ s.homepage = HOMEPAGE
26
+ s.add_dependency('merb', '>= 1.0')
27
+ s.require_path = 'lib'
28
+ s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,spec}/**/*")
29
+
30
+ end
31
+
32
+ Rake::GemPackageTask.new(spec) do |pkg|
33
+ pkg.gem_spec = spec
34
+ end
35
+
36
+ desc "install the plugin as a gem"
37
+ task :install do
38
+ Merb::RakeHelper.install(GEM_NAME, :version => GEM_VERSION)
39
+ end
40
+
41
+ desc "Uninstall the gem"
42
+ task :uninstall do
43
+ Merb::RakeHelper.uninstall(GEM_NAME, :version => GEM_VERSION)
44
+ end
45
+
46
+ desc "Create a gemspec file"
47
+ task :gemspec do
48
+ File.open("#{GEM_NAME}.gemspec", "w") do |file|
49
+ file.puts spec.to_ruby
50
+ end
51
+ end
data/TODO ADDED
@@ -0,0 +1 @@
1
+ TODO:
@@ -0,0 +1,53 @@
1
+ module Merb::Generators
2
+
3
+ class PaperclipGenerator < NamespacedGenerator
4
+
5
+ def self.source_root
6
+ File.dirname(__FILE__) / 'templates'
7
+ end
8
+
9
+ desc <<-DESC
10
+ Generators a paperclip migration
11
+ DESC
12
+
13
+ first_argument :name, :required => true, :desc => "model name"
14
+ second_argument :attachments, :required => true, :as => :array, :default => [], :desc => "space separated list of fields"
15
+
16
+ template :paperclip do
17
+ source(File.dirname(__FILE__) / 'templates' / '%file_name%.rb')
18
+ destination("schema/migrations/#{migration_file_name}.rb")
19
+ end
20
+
21
+ def version
22
+ format("%03d", current_migration_nr + 1)
23
+ end
24
+
25
+ def migration_file_name
26
+ names = migration_attachments
27
+ "#{version}_add_attachments_#{names.join("_")}_to_#{class_name.underscore}"
28
+ end
29
+
30
+ def migration_name
31
+ names = migration_attachments
32
+ "add_attachments_#{names.join("_")}_to_#{class_name.underscore}".classify
33
+ end
34
+
35
+ protected
36
+
37
+ def migration_attachments
38
+ names = attachments.map(&:underscore)
39
+ attachments.length == 1 ? names : names[0..-2] + ["and", names[-1]]
40
+ end
41
+
42
+ def destination_directory
43
+ File.join(destination_root, 'schema', 'migrations')
44
+ end
45
+
46
+ def current_migration_nr
47
+ Dir["#{destination_directory}/*"].map{|f| File.basename(f).match(/^(\d+)/)[0].to_i }.max.to_i
48
+ end
49
+
50
+ end
51
+
52
+ add :paperclip, PaperclipGenerator
53
+ end
@@ -0,0 +1,17 @@
1
+ class <%= migration_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ <% attachments.each do |attachment| -%>
4
+ add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_name, :string
5
+ add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_content_type, :string
6
+ add_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_size, :integer
7
+ <% end -%>
8
+ end
9
+
10
+ def self.down
11
+ <% attachments.each do |attachment| -%>
12
+ remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_name
13
+ remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_content_type
14
+ remove_column :<%= class_name.underscore.camelize.tableize %>, :<%= attachment %>_file_size
15
+ <% end -%>
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ def obtain_class
2
+ class_name = ENV['CLASS'] || ENV['class']
3
+ raise "Must specify CLASS" unless class_name
4
+ @klass = Object.const_get(class_name)
5
+ end
6
+
7
+ def obtain_attachments
8
+ name = ENV['ATTACHMENT'] || ENV['attachment']
9
+ raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions)
10
+ if !name.blank? && @klass.attachment_definitions.keys.include?(name)
11
+ [ name ]
12
+ else
13
+ @klass.attachment_definitions.keys
14
+ end
15
+ end
16
+
17
+ namespace :paperclip do
18
+ desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)"
19
+ task :refresh => :environment do
20
+ klass = obtain_class
21
+ names = obtain_attachments
22
+ instances = klass.find(:all)
23
+
24
+ puts "Regenerating thumbnails for #{instances.length} instances of #{klass.name}:"
25
+ instances.each do |instance|
26
+ names.each do |name|
27
+ result = if instance.send("#{ name }?")
28
+ instance.send(name).reprocess!
29
+ instance.send(name).save
30
+ else
31
+ true
32
+ end
33
+ print result ? "." : "x"; $stdout.flush
34
+ end
35
+ end
36
+ puts " Done."
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # make sure we're running inside Merb
2
+ if defined?(Merb::Plugins)
3
+ dependency "activerecord"
4
+
5
+ # Merb gives you a Merb::Plugins.config hash...feel free to put your stuff in your piece of it
6
+ Merb::Plugins.config[:merb_paperclip] = {
7
+ :chickens => false
8
+ }
9
+
10
+ Merb::BootLoader.before_app_loads do
11
+ require File.join(File.dirname(__FILE__), "paperclip")
12
+ Merb.add_generators(File.join(File.dirname(__FILE__), 'generators', 'paperclip_generator'))
13
+ end
14
+
15
+ Merb::BootLoader.after_app_loads do
16
+ end
17
+
18
+ Merb::Plugins.add_rakefiles "merb_paperclip/merbtasks"
19
+ end
@@ -0,0 +1,287 @@
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 => ":merb_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
+
47
+ logger.info("[paperclip] Paperclip attachment #{name} on #{instance.class} initialized.")
48
+ end
49
+
50
+ # What gets called when you call instance.attachment = File. It clears errors,
51
+ # assigns attributes, processes the file, and runs validations. It also queues up
52
+ # the previous file for deletion, to be flushed away on #save of its host.
53
+ # In addition to form uploads, you can also assign another Paperclip attachment:
54
+ # new_user.avatar = old_user.avatar
55
+ def assign uploaded_file
56
+ %w(file_name).each do |field|
57
+ unless @instance.class.column_names.include?("#{name}_#{field}")
58
+ raise PaperclipError.new("#{self} model does not have required column '#{name}_#{field}'")
59
+ end
60
+ end
61
+
62
+ if uploaded_file.is_a?(Paperclip::Attachment)
63
+ uploaded_file = uploaded_file.to_file(:original)
64
+ end
65
+
66
+ return nil unless valid_assignment?(uploaded_file)
67
+ logger.info("[paperclip] Assigning #{uploaded_file.inspect} to #{name}")
68
+
69
+ queue_existing_for_delete
70
+ @errors = []
71
+ @validation_errors = nil
72
+
73
+ return nil if uploaded_file.nil?
74
+
75
+ logger.info("[paperclip] Writing attributes for #{name}")
76
+ @queued_for_write[:original] = uploaded_file['tempfile']
77
+ @instance[:"#{@name}_file_name"] = uploaded_file['tempfile'].original_filename.strip.gsub /[^\w\d\.\-]+/, '_'
78
+ @instance[:"#{@name}_content_type"] = uploaded_file['tempfile'].content_type.strip
79
+ @instance[:"#{@name}_file_size"] = uploaded_file['tempfile'].size.to_i
80
+ @instance[:"#{@name}_updated_at"] = Time.now
81
+
82
+ @dirty = true
83
+
84
+ post_process
85
+ ensure
86
+ validate
87
+ end
88
+
89
+ # Returns the public URL of the attachment, with a given style. Note that this
90
+ # does not necessarily need to point to a file that your web server can access
91
+ # and can point to an action in your app, if you need fine grained security.
92
+ # This is not recommended if you don't need the security, however, for
93
+ # performance reasons.
94
+ def url style = default_style
95
+ url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
96
+ updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
97
+ end
98
+
99
+ # Returns the path of the attachment as defined by the :path option. If the
100
+ # file is stored in the filesystem the path refers to the path of the file on
101
+ # disk. If the file is stored in S3, the path is the "key" part of the URL,
102
+ # and the :bucket option refers to the S3 bucket.
103
+ def path style = nil #:nodoc:
104
+ interpolate(@path, style)
105
+ end
106
+
107
+ # Alias to +url+
108
+ def to_s style = nil
109
+ url(style)
110
+ end
111
+
112
+ # Returns true if there are no errors on this attachment.
113
+ def valid?
114
+ validate
115
+ errors.length == 0
116
+ end
117
+
118
+ # Returns an array containing the errors on this attachment.
119
+ def errors
120
+ @errors.compact.uniq
121
+ end
122
+
123
+ # Returns true if there are changes that need to be saved.
124
+ def dirty?
125
+ @dirty
126
+ end
127
+
128
+ # Saves the file, if there are no errors. If there are, it flushes them to
129
+ # the instance's errors and returns false, cancelling the save.
130
+ def save
131
+ if valid?
132
+ logger.info("[paperclip] Saving files for #{name}")
133
+ flush_deletes
134
+ flush_writes
135
+ @dirty = false
136
+ true
137
+ else
138
+ logger.info("[paperclip] Errors on #{name}. Not saving.")
139
+ flush_errors
140
+ false
141
+ end
142
+ end
143
+
144
+ # Returns the name of the file as originally assigned, and as lives in the
145
+ # <attachment>_file_name attribute of the model.
146
+ def original_filename
147
+ instance[:"#{name}_file_name"]
148
+ end
149
+
150
+ def updated_at
151
+ time = instance[:"#{name}_updated_at"]
152
+ time && time.to_i
153
+ end
154
+
155
+ # A hash of procs that are run during the interpolation of a path or url.
156
+ # A variable of the format :name will be replaced with the return value of
157
+ # the proc named ":name". Each lambda takes the attachment and the current
158
+ # style as arguments. This hash can be added to with your own proc if
159
+ # necessary.
160
+ def self.interpolations
161
+ @interpolations ||= {
162
+ :merb_root => lambda{|attachment,style| Merb.root },
163
+ :class => lambda do |attachment,style|
164
+ attachment.instance.class.name.underscore.pluralize
165
+ end,
166
+ :basename => lambda do |attachment,style|
167
+ attachment.original_filename.gsub(File.extname(attachment.original_filename), "")
168
+ end,
169
+ :extension => lambda do |attachment,style|
170
+ ((style = attachment.styles[style]) && style.last) ||
171
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
172
+ end,
173
+ :id => lambda{|attachment,style| attachment.instance.id },
174
+ :id_partition => lambda do |attachment, style|
175
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
176
+ end,
177
+ :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
178
+ :style => lambda{|attachment,style| style || attachment.default_style },
179
+ }
180
+ end
181
+
182
+ # This method really shouldn't be called that often. It's expected use is in the
183
+ # paperclip:refresh rake task and that's it. It will regenerate all thumbnails
184
+ # forcefully, by reobtaining the original file and going through the post-process
185
+ # again.
186
+ def reprocess!
187
+ new_original = Tempfile.new("paperclip-reprocess")
188
+ if old_original = to_file(:original)
189
+ new_original.write( old_original.read )
190
+ new_original.rewind
191
+
192
+ @queued_for_write = { :original => new_original }
193
+ post_process
194
+
195
+ old_original.close if old_original.respond_to?(:close)
196
+
197
+ save
198
+ else
199
+ true
200
+ end
201
+ end
202
+
203
+ def file?
204
+ !original_filename.blank?
205
+ end
206
+
207
+ private
208
+
209
+ def logger
210
+ instance.logger
211
+ end
212
+
213
+ def valid_assignment? file #:nodoc:
214
+ file.nil? || (file.is_a?(Mash) && file.has_key?(:tempfile))
215
+ end
216
+
217
+ def validate #:nodoc:
218
+ unless @validation_errors
219
+ @validation_errors = @validations.collect do |v|
220
+ v.call(self, instance)
221
+ end.flatten.compact.uniq
222
+ @errors += @validation_errors
223
+ end
224
+ @validation_errors
225
+ end
226
+
227
+ def normalize_style_definition
228
+ @styles.each do |name, args|
229
+ dimensions, format = [args, nil].flatten[0..1]
230
+ format = nil if format == ""
231
+ @styles[name] = [dimensions, format]
232
+ end
233
+ end
234
+
235
+ def initialize_storage
236
+ @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
237
+ self.extend(@storage_module)
238
+ end
239
+
240
+ def post_process #:nodoc:
241
+ return if @queued_for_write[:original].nil?
242
+ logger.info("[paperclip] Post-processing #{name}")
243
+ @styles.each do |name, args|
244
+ begin
245
+ dimensions, format = args
246
+ dimensions = dimensions.call(instance) if dimensions.respond_to? :call
247
+ @queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
248
+ dimensions,
249
+ format,
250
+ @whiny_thumnails)
251
+ rescue PaperclipError => e
252
+ @errors << e.message if @whiny_thumbnails
253
+ end
254
+ end
255
+ end
256
+
257
+ def interpolate pattern, style = default_style #:nodoc:
258
+ interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
259
+ interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
260
+ tag, blk = interpolation
261
+ result.gsub(/:#{tag}/) do |match|
262
+ blk.call( self, style )
263
+ end
264
+ end
265
+ end
266
+
267
+ def queue_existing_for_delete #:nodoc:
268
+ return unless file?
269
+ logger.info("[paperclip] Queueing the existing files for #{name} for deletion.")
270
+ @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
271
+ path(style) if exists?(style)
272
+ end.compact
273
+ @instance[:"#{@name}_file_name"] = nil
274
+ @instance[:"#{@name}_content_type"] = nil
275
+ @instance[:"#{@name}_file_size"] = nil
276
+ @instance[:"#{@name}_updated_at"] = nil
277
+ end
278
+
279
+ def flush_errors #:nodoc:
280
+ @errors.each do |error|
281
+ instance.errors.add(name, error)
282
+ end
283
+ end
284
+
285
+ end
286
+ end
287
+
@@ -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
+
80
+ if crop
81
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
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,47 @@
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
+ # 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
+ class Mash
38
+ include IOStream
39
+ end
40
+
41
+ %w( Tempfile StringIO ).each do |klass|
42
+ if Object.const_defined? klass
43
+ Object.const_get(klass).class_eval do
44
+ include IOStream
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,204 @@
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
+ # ":merb_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
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
+ logger.info("[paperclip] Writing files for #{name}")
40
+ @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
+ file.close
45
+ result.close
46
+ end
47
+ @queued_for_write = {}
48
+ end
49
+
50
+ def flush_deletes #:nodoc:
51
+ logger.info("[paperclip] Deleting files for #{name}")
52
+ @queued_for_delete.each do |path|
53
+ begin
54
+ logger.info("[paperclip] -> #{path}")
55
+ FileUtils.rm(path) if File.exist?(path)
56
+ rescue Errno::ENOENT => e
57
+ # ignore file-not-found, let everything else pass
58
+ end
59
+ end
60
+ @queued_for_delete = []
61
+ end
62
+ end
63
+
64
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
65
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
66
+ # There are a few S3-specific options for has_attached_file:
67
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
68
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
69
+ # gives you. You can 'environment-space' this just like you do to your
70
+ # database.yml file, so different environments can use different accounts:
71
+ # development:
72
+ # access_key_id: 123...
73
+ # secret_access_key: 123...
74
+ # test:
75
+ # access_key_id: abc...
76
+ # secret_access_key: abc...
77
+ # production:
78
+ # access_key_id: 456...
79
+ # secret_access_key: 456...
80
+ # This is not required, however, and the file may simply look like this:
81
+ # access_key_id: 456...
82
+ # secret_access_key: 456...
83
+ # In which case, those access keys will be used in all environments.
84
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
85
+ # policies that S3 provides (more information can be found here:
86
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
87
+ # The default for Paperclip is "public-read".
88
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
89
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
90
+ # default), and 'https' when your :s3_permissions are anything else.
91
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
92
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
93
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
94
+ # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
95
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
96
+ # Normally, this won't matter in the slightest and you can leave the default (which is
97
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
98
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
99
+ # * +path+: This is the key under the bucket in which the file will be stored. The
100
+ # URL will be constructed from the bucket and the path. This is what you will want
101
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
102
+ # S3 (strictly speaking) does not support directories, you can still use a / to
103
+ # separate parts of your file name.
104
+ module S3
105
+ def self.extended base
106
+ require 'right_aws'
107
+ base.instance_eval do
108
+ @bucket = @options[:bucket]
109
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
110
+ @s3_options = @options[:s3_options] || {}
111
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
112
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
113
+ @url = ":s3_path_url" unless @url.to_s.match(/^s3.*url$/)
114
+ end
115
+ base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
116
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
117
+ end
118
+ base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
119
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
120
+ end
121
+ ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
122
+ end
123
+
124
+ def s3
125
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
126
+ @s3_credentials[:secret_access_key],
127
+ @s3_options)
128
+ end
129
+
130
+ def s3_bucket
131
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
132
+ end
133
+
134
+ def bucket_name
135
+ @bucket
136
+ end
137
+
138
+ def parse_credentials creds
139
+ creds = find_credentials(creds).stringify_keys
140
+ (creds[Merb.env] || creds).symbolize_keys
141
+ end
142
+
143
+ def exists?(style = default_style)
144
+ s3_bucket.key(path(style)) ? true : false
145
+ end
146
+
147
+ def s3_protocol
148
+ @s3_protocol
149
+ end
150
+
151
+ # Returns representation of the data of the file assigned to the given
152
+ # style, in the format most representative of the current storage.
153
+ def to_file style = default_style
154
+ @queued_for_write[style] || s3_bucket.key(path(style))
155
+ end
156
+ alias_method :to_io, :to_file
157
+
158
+ def flush_writes #:nodoc:
159
+ logger.info("[paperclip] Writing files for #{name}")
160
+ @queued_for_write.each do |style, file|
161
+ begin
162
+ logger.info("[paperclip] -> #{path(style)}")
163
+ key = s3_bucket.key(path(style))
164
+ key.data = file
165
+ key.put(nil, @s3_permissions)
166
+ rescue RightAws::AwsError => e
167
+ raise
168
+ end
169
+ end
170
+ @queued_for_write = {}
171
+ end
172
+
173
+ def flush_deletes #:nodoc:
174
+ logger.info("[paperclip] Writing files for #{name}")
175
+ @queued_for_delete.each do |path|
176
+ begin
177
+ logger.info("[paperclip] -> #{path}")
178
+ if file = s3_bucket.key(path)
179
+ file.delete
180
+ end
181
+ rescue RightAws::AwsError
182
+ # Ignore this.
183
+ end
184
+ end
185
+ @queued_for_delete = []
186
+ end
187
+
188
+ def find_credentials creds
189
+ case creds
190
+ when File:
191
+ YAML.load_file(creds.path)
192
+ when String:
193
+ YAML.load_file(creds)
194
+ when Hash:
195
+ creds
196
+ else
197
+ raise ArgumentError, "Credentials are not a path, file, or hash."
198
+ end
199
+ end
200
+ private :find_credentials
201
+
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,80 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail
4
+
5
+ attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny_thumbnails+ is true (which it is, by default.
12
+ def initialize file, target_geometry, format = nil, whiny_thumbnails = true
13
+ @file = file
14
+ @crop = target_geometry[-1,1] == '#'
15
+ @target_geometry = Geometry.parse target_geometry
16
+ @current_geometry = Geometry.from_file file
17
+ @whiny_thumbnails = whiny_thumbnails
18
+
19
+ @current_format = File.extname(@file.path)
20
+ @basename = File.basename(@file.path, @current_format)
21
+
22
+ @format = format
23
+ end
24
+
25
+ # Creates a thumbnail, as specified in +initialize+, +make+s it, and returns the
26
+ # resulting Tempfile.
27
+ def self.make file, dimensions, format = nil, whiny_thumbnails = true
28
+ new(file, dimensions, format, whiny_thumbnails).make
29
+ end
30
+
31
+ # Returns true if the +target_geometry+ is meant to crop.
32
+ def crop?
33
+ @crop
34
+ end
35
+
36
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
37
+ # that contains the new image.
38
+ def make
39
+ src = @file
40
+ dst = Tempfile.new([@basename, @format].compact.join("."))
41
+ dst.binmode
42
+
43
+ command = <<-end_command
44
+ #{ Paperclip.path_for_command('convert') }
45
+ "#{ File.expand_path(src.path) }"
46
+ #{ transformation_command }
47
+ "#{ File.expand_path(dst.path) }"
48
+ end_command
49
+ success = system(command.gsub(/\s+/, " "))
50
+
51
+ if success && $?.exitstatus != 0 && @whiny_thumbnails
52
+ raise PaperclipError, "There was an error processing this thumbnail"
53
+ end
54
+
55
+ dst
56
+ end
57
+
58
+ # Returns the command ImageMagick's +convert+ needs to transform the image
59
+ # into the thumbnail.
60
+ def transformation_command
61
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
62
+ trans = "-scale \"#{scale}\""
63
+ trans << " -crop \"#{crop}\" +repage" if crop
64
+ trans
65
+ end
66
+ end
67
+
68
+ # Due to how ImageMagick handles its image format conversion and how Tempfile
69
+ # handles its naming scheme, it is necessary to override how Tempfile makes
70
+ # its names so as to allow for file extensions. Idea taken from the comments
71
+ # on this blog post:
72
+ # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
73
+ class Tempfile < ::Tempfile
74
+ # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
75
+ def make_tmpname(basename, n)
76
+ extension = File.extname(basename)
77
+ sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ module Paperclip
2
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
3
+ # to the +File+ class. Useful for testing.
4
+ # user.avatar = File.new("test/test_avatar.jpg")
5
+ module Upfile
6
+
7
+ # Infer the MIME-type of the file from the extension.
8
+ def content_type
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
+ case type
11
+ when %r"jpe?g" then "image/jpeg"
12
+ when %r"tiff?" then "image/tiff"
13
+ when %r"png", "gif", "bmp" then "image/#{type}"
14
+ when "txt" then "text/plain"
15
+ when %r"html?" then "text/html"
16
+ when "csv", "xml", "css", "js" then "text/#{type}"
17
+ else "application/x-#{type}"
18
+ end
19
+ end
20
+
21
+ # Returns the file's normal name.
22
+ def original_filename
23
+ File.basename(self.path)
24
+ end
25
+
26
+ # Returns the size of the file.
27
+ def size
28
+ File.size(self)
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ class File #:nodoc:
35
+ include Paperclip::Upfile
36
+ end
37
+
data/lib/paperclip.rb ADDED
@@ -0,0 +1,246 @@
1
+ # Paperclip allows file attachments that are stored in the filesystem. All graphical
2
+ # transformations are done using the Graphics/ImageMagick command line utilities and
3
+ # are stored in Tempfiles until the record is saved. Paperclip does not require a
4
+ # separate model for storing the attachment's information, instead adding a few simple
5
+ # columns to your table.
6
+ #
7
+ # Author:: Jon Yurek
8
+ # Copyright:: Copyright (c) 2008 thoughtbot, inc.
9
+ # License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
10
+ #
11
+ # Paperclip defines an attachment as any file, though it makes special considerations
12
+ # for image files. You can declare that a model has an attached file with the
13
+ # +has_attached_file+ method:
14
+ #
15
+ # class User < ActiveRecord::Base
16
+ # has_attached_file :avatar, :styles => { :thumb => "100x100" }
17
+ # end
18
+ #
19
+ # user = User.new
20
+ # user.avatar = params[:user][:avatar]
21
+ # user.avatar.url
22
+ # # => "/users/avatars/4/original_me.jpg"
23
+ # user.avatar.url(:thumb)
24
+ # # => "/users/avatars/4/thumb_me.jpg"
25
+ #
26
+ # See the +has_attached_file+ documentation for more details.
27
+
28
+ require 'tempfile'
29
+ require 'paperclip/upfile'
30
+ require 'paperclip/iostream'
31
+ require 'paperclip/geometry'
32
+ require 'paperclip/thumbnail'
33
+ require 'paperclip/storage'
34
+ require 'paperclip/attachment'
35
+
36
+ # The base module that gets included in ActiveRecord::Base. See the
37
+ # documentation for Paperclip::ClassMethods for more useful information.
38
+ module Paperclip
39
+
40
+ VERSION = "2.1.2"
41
+
42
+ class << self
43
+ # Provides configurability to Paperclip. There are a number of options available, such as:
44
+ # * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of
45
+ # an uploaded image. Defaults to true.
46
+ # * image_magick_path: Defines the path at which to find the +convert+ and +identify+
47
+ # programs if they are not visible to Merb the system's search path. Defaults to
48
+ # nil, which uses the first executable found in the search path.
49
+ def options
50
+ @options ||= {
51
+ :whiny_thumbnails => true,
52
+ :image_magick_path => nil
53
+ }
54
+ end
55
+
56
+ def path_for_command command #:nodoc:
57
+ path = [options[:image_magick_path], command].compact
58
+ File.join(*path)
59
+ end
60
+
61
+ def included base #:nodoc:
62
+ base.extend ClassMethods
63
+ end
64
+ end
65
+
66
+ class PaperclipError < StandardError #:nodoc:
67
+ end
68
+
69
+ class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
70
+ end
71
+
72
+ module ClassMethods
73
+ # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
74
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
75
+ # The attribute returns a Paperclip::Attachment object which handles the management of
76
+ # that file. The intent is to make the attachment as much like a normal attribute. The
77
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
78
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
79
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
80
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
81
+ # you can set to change the behavior of a Paperclip attachment:
82
+ # * +url+: The full URL of where the attachment is publically accessible. This can just
83
+ # as easily point to a directory served directly through Apache as it can to an action
84
+ # that can control permissions. You can specify the full domain and path, but usually
85
+ # just an absolute path is sufficient. The leading slash must be included manually for
86
+ # absolute paths. The default value is "/:class/:attachment/:id/:style_:filename". See
87
+ # Paperclip::Attachment#interpolate for more information on variable interpolaton.
88
+ # :url => "/:attachment/:id/:style_:basename:extension"
89
+ # :url => "http://some.other.host/stuff/:class/:id_:extension"
90
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
91
+ # This field is interpolated just as the url is. The default value is
92
+ # "/:class/:attachment/missing_:style.png"
93
+ # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
94
+ # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
95
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
96
+ # geometry strings at the ImageMagick website
97
+ # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
98
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
99
+ # inside the dimensions and then crop the rest off (weighted at the center). The
100
+ # default value is to generate no thumbnails.
101
+ # * +default_style+: The thumbnail style that will be used by default URLs.
102
+ # Defaults to +original+.
103
+ # has_attached_file :avatar, :styles => { :normal => "100x100#" },
104
+ # :default_style => :normal
105
+ # user.avatar.url # => "/avatars/23/normal_me.png"
106
+ # * +whiny_thumbnails+: Will raise an error if Paperclip cannot process thumbnails of an
107
+ # uploaded image. This will ovrride the global setting for this attachment.
108
+ # Defaults to true.
109
+ # * +storage+: Chooses the storage backend where the files will be stored. The current
110
+ # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
111
+ # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
112
+ # for backend-specific options.
113
+ def has_attached_file name, options = {}
114
+ include InstanceMethods
115
+
116
+ write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil?
117
+ attachment_definitions[name] = {:validations => []}.merge(options)
118
+
119
+ after_save :save_attached_files
120
+ before_destroy :destroy_attached_files
121
+
122
+ define_method name do |*args|
123
+ a = attachment_for(name)
124
+ (args.length > 0) ? a.to_s(args.first) : a
125
+ end
126
+
127
+ define_method "#{name}=" do |file|
128
+ attachment_for(name).assign(file)
129
+ end
130
+
131
+ define_method "#{name}?" do
132
+ attachment_for(name).file?
133
+ end
134
+
135
+ validates_each(name) do |record, attr, value|
136
+ value.send(:flush_errors) unless value.valid?
137
+ end
138
+ end
139
+
140
+ # Places ActiveRecord-style validations on the size of the file assigned. The
141
+ # possible options are:
142
+ # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
143
+ # * +less_than+: equivalent to :in => 0..options[:less_than]
144
+ # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
145
+ # * +message+: error message to display, use :min and :max as replacements
146
+ def validates_attachment_size name, options = {}
147
+ attachment_definitions[name][:validations] << lambda do |attachment, instance|
148
+ unless options[:greater_than].nil?
149
+ options[:in] = (options[:greater_than]..(1/0)) # 1/0 => Infinity
150
+ end
151
+ unless options[:less_than].nil?
152
+ options[:in] = (0..options[:less_than])
153
+ end
154
+
155
+ if attachment.file? && !options[:in].include?(instance[:"#{name}_file_size"].to_i)
156
+ min = options[:in].first
157
+ max = options[:in].last
158
+
159
+ if options[:message]
160
+ options[:message].gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
161
+ else
162
+ "file size is not between #{min} and #{max} bytes."
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
169
+ def validates_attachment_thumbnails name, options = {}
170
+ attachment_definitions[name][:whiny_thumbnails] = true
171
+ end
172
+
173
+ # Places ActiveRecord-style validations on the presence of a file.
174
+ def validates_attachment_presence name, options = {}
175
+ attachment_definitions[name][:validations] << lambda do |attachment, instance|
176
+ unless attachment.file?
177
+ options[:message] || "must be set."
178
+ end
179
+ end
180
+ end
181
+
182
+ # Places ActiveRecord-style validations on the content type of the file assigned. The
183
+ # possible options are:
184
+ # * +content_type+: Allowed content types. Can be a single content type or an array.
185
+ # Each type can be a String or a Regexp. It should be noted that Internet Explorer uploads
186
+ # files with content_types that you may not expect. For example, JPEG images are given
187
+ # image/pjpeg and PNGs are image/x-png, so keep that in mind when determining how you match.
188
+ # Allows all by default.
189
+ # * +message+: The message to display when the uploaded file has an invalid content type.
190
+ def validates_attachment_content_type name, options = {}
191
+ attachment_definitions[name][:validations] << lambda do |attachment, instance|
192
+ valid_types = [options[:content_type]].flatten
193
+
194
+ unless attachment.original_filename.nil?
195
+ unless options[:content_type].blank?
196
+ content_type = instance[:"#{name}_content_type"]
197
+ unless valid_types.any?{|t| t === content_type }
198
+ options[:message] || "is not one of the allowed file types."
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ # Returns the attachment definitions defined by each call to has_attached_file.
206
+ def attachment_definitions
207
+ read_inheritable_attribute(:attachment_definitions)
208
+ end
209
+
210
+ end
211
+
212
+ module InstanceMethods #:nodoc:
213
+ def attachment_for name
214
+ @attachments ||= {}
215
+ @attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
216
+ end
217
+
218
+ def each_attachment
219
+ self.class.attachment_definitions.each do |name, definition|
220
+ yield(name, attachment_for(name))
221
+ end
222
+ end
223
+
224
+ def save_attached_files
225
+ logger.info("[paperclip] Saving attachments.")
226
+ each_attachment do |name, attachment|
227
+ attachment.send(:save)
228
+ end
229
+ end
230
+
231
+ def destroy_attached_files
232
+ logger.info("[paperclip] Deleting attachments.")
233
+ each_attachment do |name, attachment|
234
+ attachment.send(:queue_existing_for_delete)
235
+ attachment.send(:flush_deletes)
236
+ end
237
+ end
238
+ end
239
+
240
+ end
241
+
242
+ # Set it all up.
243
+ if Object.const_defined?("ActiveRecord")
244
+ ActiveRecord::Base.send(:include, Paperclip)
245
+ File.send(:include, Paperclip::Upfile)
246
+ end
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "merb_paperclip" do
4
+ it "should do nothing" do
5
+ true.should == true
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeremydurham-merb_paperclip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.12
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Durham
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-01 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: merb
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.4
23
+ version:
24
+ description: A Merb plugin that is essentially a port of Jon Yurek's paperclip
25
+ email: jeremydurham@gmail.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README
32
+ - LICENSE
33
+ - TODO
34
+ files:
35
+ - LICENSE
36
+ - README
37
+ - Rakefile
38
+ - TODO
39
+ - lib/generators
40
+ - lib/generators/paperclip_generator.rb
41
+ - lib/generators/templates
42
+ - lib/generators/templates/%file_name%.rb
43
+ - lib/merb_paperclip
44
+ - lib/merb_paperclip/merbtasks.rb
45
+ - lib/merb_paperclip.rb
46
+ - lib/paperclip
47
+ - lib/paperclip/attachment.rb
48
+ - lib/paperclip/geometry.rb
49
+ - lib/paperclip/iostream.rb
50
+ - lib/paperclip/storage.rb
51
+ - lib/paperclip/thumbnail.rb
52
+ - lib/paperclip/upfile.rb
53
+ - lib/paperclip.rb
54
+ - spec/merb_paperclip_spec.rb
55
+ - spec/spec_helper.rb
56
+ has_rdoc: true
57
+ homepage: http://www.thoughtbot.com/projects/paperclip/
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project: merb
78
+ rubygems_version: 1.2.0
79
+ signing_key:
80
+ specification_version: 2
81
+ summary: A Merb plugin that is essentially a port of Jon Yurek's paperclip
82
+ test_files: []
83
+