coryodaniel-milton 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,202 @@
1
+ require 'fileutils'
2
+ require 'milton/derivatives/derivative'
3
+
4
+ module Milton
5
+ module Attachment
6
+ # Call is_attachment with your options in order to add attachment
7
+ # functionality to your ActiveRecord model.
8
+ #
9
+ # TODO: list options
10
+ def is_attachment(options={})
11
+ # Check to see that it hasn't already been extended so that subclasses
12
+ # can redefine is_attachment from their superclasses and overwrite
13
+ # options w/o losing the superclass options.
14
+ unless respond_to?(:has_attachment_methods)
15
+ extend Milton::Attachment::AttachmentMethods
16
+ class_inheritable_accessor :milton_options
17
+ end
18
+ has_attachment_methods(options)
19
+ end
20
+
21
+ module AttachmentMethods
22
+ def require_column(column, message)
23
+ begin
24
+ raise message unless column_names.include?(column)
25
+ rescue ActiveRecord::StatementInvalid => i
26
+ # table doesn't exist yet, i.e. hasn't been migrated in...
27
+ end
28
+ end
29
+
30
+ def has_attachment_methods(options={})
31
+ require_column 'filename', "Milton requires a filename column on #{class_name} table"
32
+
33
+ # It's possible that this is being included from a class and a sub
34
+ # class of that class, in which case we need to merge the existing
35
+ # options up.
36
+ self.milton_options ||= {}
37
+ milton_options.merge!(options)
38
+
39
+ # Character used to seperate a filename from its derivative options, this
40
+ # character will be stripped from all incoming filenames and replaced by
41
+ # replacement
42
+ milton_options[:separator] ||= '.'
43
+ milton_options[:replacement] ||= '-'
44
+ milton_options[:tempfile_path] ||= File.join(Rails.root, "tmp", "milton")
45
+ milton_options[:storage] ||= :disk
46
+ milton_options[:storage_options] ||= {}
47
+ milton_options[:processors] ||= {}
48
+ milton_options[:uploading] ||= true
49
+
50
+ # Set to true to allow on-demand processing of derivatives. This can
51
+ # be rediculously slow because it requires that the existance of the
52
+ # derivative is checked each time it's requested -- throw in S3 and
53
+ # that becomes a huge lag. Reccommended only for prototyping.
54
+ milton_options[:postproccess] ||= false
55
+
56
+ # TODO: Write about recipes
57
+ # * They're refered to by name in #path
58
+ # * They're an order of derivations to make against this attachment
59
+ # * They run in the order defined
60
+ # * They are created and run when the AR model is created
61
+ # * They're necessary when +:postprocessing+ is turned off
62
+ milton_options[:recipes] ||= {}
63
+ milton_options[:recipes].each do |name, steps|
64
+ steps = [steps] unless steps.is_a?(Array)
65
+ steps.each do |step|
66
+ step.each { |processor, options| Milton.try_require "milton/derivatives/#{processor}", "No '#{processor}' processor found for Milton" }
67
+ end
68
+ end
69
+
70
+ # TODO: Write about storage options
71
+ # * Late binding (so right_aws is only req'd if you use S3)
72
+ Milton.try_require "milton/storage/#{milton_options[:storage]}_file", "No '#{milton_options[:storage]}' storage found for Milton"
73
+
74
+ # TODO: initialize these options in DiskFile
75
+ if milton_options[:storage] == :disk
76
+ # root of where the underlying files are stored (or will be stored)
77
+ # on the file system
78
+ milton_options[:storage_options][:root] ||= File.join(Rails.root, "public", table_name)
79
+ milton_options[:storage_options][:root] = File.expand_path(milton_options[:storage_options][:root])
80
+ # mode to set on stored files and created directories
81
+ milton_options[:storage_options][:chmod] ||= 0755
82
+ end
83
+
84
+ validates_presence_of :filename
85
+
86
+ after_destroy :destroy_attached_file
87
+ after_create :create_derivatives
88
+
89
+ include Milton::Attachment::InstanceMethods
90
+
91
+ if milton_options[:uploading]
92
+ require 'milton/uploading'
93
+ extend Milton::Uploading::ClassMethods
94
+ include Milton::Uploading::InstanceMethods
95
+ end
96
+ end
97
+ end
98
+
99
+ # These get mixed in to your model when you use Milton
100
+ module InstanceMethods
101
+ # Sets the filename to the given filename (sanitizes the given filename
102
+ # as well)
103
+ #
104
+ # TODO: change the filename on the underlying file system on save so as
105
+ # not to orphan the file
106
+ def filename=(name)
107
+ write_attribute :filename, Storage::StoredFile.sanitize_filename(name, self.class.milton_options)
108
+ end
109
+
110
+ # Returns the content_type of this attachment, tries to determine it if
111
+ # hasn't been determined yet or is not saved to the database
112
+ def content_type
113
+ return self[:content_type] unless self[:content_type].blank?
114
+ self.content_type = attached_file.mime_type
115
+ end
116
+
117
+ # Sets the content type to the given type
118
+ def content_type=(type)
119
+ write_attribute :content_type, type.to_s.strip
120
+ end
121
+
122
+ # Simple helper, same as path except returns the directory from
123
+ # .../public/ on, i.e. for showing images in your views.
124
+ #
125
+ # @asset.path => /var/www/site/public/assets/000/000/001/313/milton.jpg
126
+ # @asset.public_path => /assets/000/000/001/313/milton.jpg
127
+ #
128
+ # Can send a different base path than public if you want to give the
129
+ # path from that base on, useful if you change your root path to
130
+ # somewhere else.
131
+ def public_path(options={}, base='public')
132
+ path(options).gsub(/.*?\/#{base}/, '')
133
+ end
134
+
135
+ # The path to the file.
136
+ def path(options=nil)
137
+ return attached_file.path if options.nil?
138
+ process(options).path
139
+ end
140
+
141
+ protected
142
+
143
+ # Meant to be used as an after_create filter -- loops over all the
144
+ # recipes and processes them to create the derivatives.
145
+ def create_derivatives
146
+ milton_options[:recipes].each{ |name, recipe| process(name, true) } if milton_options[:recipes].any?
147
+ end
148
+
149
+ # Process the given options to produce a final derivative. +options+
150
+ # takes a Hash of options to process or the name of a pre-defined
151
+ # recipe which will be looked up and processed.
152
+ #
153
+ # Pass +force = true+ to force processing regardless of if
154
+ # +:postprocessing+ is turned on or not.
155
+ #
156
+ # Returns the final Derivative of all processors in the recipe.
157
+ def process(options, force=false)
158
+ options = milton_options[:recipes][options] unless options.is_a?(Hash)
159
+ options = [options] unless options.is_a?(Array)
160
+
161
+ source = attached_file
162
+ options.each do |recipe|
163
+ recipe.each do |processor, opts|
164
+ source = Derivative.factory(processor, source, opts, self.class.milton_options).process_if(process? || force).file
165
+ end
166
+ end
167
+ source
168
+ end
169
+
170
+ # Returns true if derivaties of the attachment should be processed,
171
+ # returns false if no processing should be done when a derivative is
172
+ # requested.
173
+ #
174
+ # No processing also means the derivative won't be checked for
175
+ # existance (since that can be slow) so w/o postprocessing things will
176
+ # be much faster but #path will happily return the paths to Derivatives
177
+ # which don't exist.
178
+ #
179
+ # It is highly recommended that you turn +:postprocessing+ off for
180
+ # anything but prototyping, and instead use recipes and refer to them
181
+ # via #path. +:postprocessing+ relies on checking for existance which
182
+ # will kill any real application.
183
+ def process?
184
+ self.class.milton_options[:postprocessing]
185
+ end
186
+
187
+ # A reference to the attached file, this is probably what you want to
188
+ # overwrite to introduce a new behavior
189
+ #
190
+ # i.e.
191
+ # have attached_file return a ResizeableFile, or a TranscodableFile
192
+ def attached_file
193
+ @attached_file ||= Storage::StoredFile.adapter(self.class.milton_options[:storage]).new(filename, id, self.class.milton_options)
194
+ end
195
+
196
+ # Clean the file from the filesystem
197
+ def destroy_attached_file
198
+ attached_file.destroy
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'mimetype_fu'
3
+ rescue MissingSourceFile
4
+ end
5
+
6
+ module Milton
7
+ class File < ::File
8
+ class << self
9
+ def extension(filename)
10
+ extension = extname(filename)
11
+ extension.slice(1, extension.length-1)
12
+ end
13
+
14
+ # File respond_to?(:mime_type) is true if mimetype_fu is installed, so
15
+ # this way we always have File.mime_type? available but it favors
16
+ # mimetype_fu's implementation.
17
+ def mime_type?(file)
18
+ ::File.respond_to?(:mime_type?) ? super(file.filename) : file.content_type
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ require 'tempfile'
2
+
3
+ module Milton
4
+ # For lack of a better name, a MiltonTempfile adds some helpful
5
+ # functionality to Ruby's Tempfile
6
+ class Tempfile < ::Tempfile
7
+ class << self
8
+ def create(data_or_path, tempfile_path)
9
+ FileUtils.mkdir_p(tempfile_path) unless File.exists?(tempfile_path)
10
+
11
+ tempfile = new(basename, tempfile_path)
12
+
13
+ if data_or_path.is_a?(StringIO)
14
+ tempfile.binmode
15
+ tempfile.write data_or_path.read
16
+ tempfile.close
17
+ else
18
+ tempfile.close
19
+ FileUtils.cp((data_or_path.respond_to?(:path) ? data_or_path.path : data_or_path), tempfile.path)
20
+ end
21
+
22
+ tempfile
23
+ end
24
+
25
+ def basename
26
+ "#{rand(Time.now.to_i)}"
27
+ end
28
+
29
+ def filename(extension)
30
+ "#{basename}.#{extension}"
31
+ end
32
+
33
+ def path(tempfile_path, extension)
34
+ File.join(tempfile_path, filename(extension))
35
+ end
36
+
37
+ # Simple helper that returns a path to a tempfile with a uniquely
38
+ # generated basename and same extension as the given source.
39
+ def from(source)
40
+ filename(Milton::File.extension(source))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,106 @@
1
+ module Milton
2
+ # Represents a file created on the file system that is a derivative of the
3
+ # one referenced by the model, i.e. a thumbnail of an image, or a transcode
4
+ # of a video.
5
+ #
6
+ # Provides a container for options and a uniform API to dealing with
7
+ # passing options for the creation of derivatives.
8
+ #
9
+ # Files created as derivatives have their creation options appended into
10
+ # their filenames so it can be checked later if a file w/ the given
11
+ # options already exists (so as not to create it again).
12
+ #
13
+ class Derivative
14
+ class << self
15
+ # Given a string of attachment options, splits them out into a hash,
16
+ # useful for things that take options on the query string or from
17
+ # filenames
18
+ def options_from(string)
19
+ Hash[*(string.split('_').collect { |option|
20
+ key, value = option.split('=')
21
+ [ key.to_sym, value || true ] # nothing on RHS of = means it's a boolean true
22
+ }).flatten]
23
+ end
24
+
25
+ # Given a filename (presumably with options embedded in it) parses out
26
+ # the options and returns them as a hash.
27
+ def extract_options_from(filename, options)
28
+ File.basename(filename, File.extname(filename))[(filename.rindex(options[:separator]) + 1)..-1]
29
+ end
30
+
31
+ # Creates a new Derivative from the given filename by extracting the
32
+ # options.
33
+ def from_filename(filename)
34
+ Derivative.new(filename, options_from(extract_options_from(filename)))
35
+ end
36
+
37
+ def process(source, options={}, settings={})
38
+ returning(derivative = new(source, options, settings)) do
39
+ derivative.process unless derivative.exists?
40
+ end
41
+ end
42
+
43
+ def factory(type, source, options={}, settings={})
44
+ begin
45
+ klass = "Milton::#{type.to_s.classify}".constantize
46
+ rescue NameError
47
+ begin
48
+ require "milton/derivatives/#{type.to_s}"
49
+ rescue MissingSourceFile => e
50
+ raise MissingSourceFile.new("#{e.message} (milton: couldn't find the processor '#{type}' you were trying to load)", e.path)
51
+ end
52
+ klass = "Milton::#{type.to_s.classify}".constantize
53
+ end
54
+
55
+ klass.new(source, options, settings)
56
+ end
57
+ end
58
+
59
+ attr_reader :options, :settings
60
+
61
+ # Instantiate a new Derivative:
62
+ # * +source+: a reference to the Storage::StoredFile this will be a Derivative of
63
+ # * +options+: options to generate the Derivative using
64
+ # * +settings+: settings about how to create Derivatives
65
+ def initialize(source, options={}, settings={})
66
+ @source = source
67
+ @options = options.is_a?(String) ? self.class.options_from(options) : options
68
+ @settings = settings
69
+ end
70
+
71
+ # The resulting filename of this Derivative with embedded options.
72
+ def filename
73
+ # ignore false booleans and don't output =true for true booleans,
74
+ # otherwise just k=v
75
+ append = options.reject{ |k, v| v.is_a?(FalseClass) }.collect { |k, v| v === true ? k.to_s : "#{k}=#{v}" }.sort.join('_')
76
+ extension = File.extname(@source.path)
77
+ File.basename(@source.path, extension) + (append.blank? ? '' : "#{settings[:separator]}#{append}") + extension
78
+ end
79
+
80
+ # The full path and filename to this Derivative.
81
+ def path
82
+ file.path
83
+ end
84
+
85
+ # Returns true if this Derivative has already been generated and stored.
86
+ def exists?
87
+ file.exists?
88
+ end
89
+
90
+ # Overwrite this to provide your derivatives processing.
91
+ def process;end;
92
+
93
+ # Convenience method, only runs process if the given condition is true.
94
+ # Returns the derivative so it's chainable.
95
+ def process_if(condition)
96
+ process if condition && !exists?
97
+ return self
98
+ end
99
+
100
+ # Returns the StoredFile which represents the Derivative (which is a copy
101
+ # of the source w/ a different filename).
102
+ def file
103
+ @file ||= @source.clone(filename)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,39 @@
1
+ require 'milton/derivatives/thumbnail/image'
2
+ require 'milton/derivatives/thumbnail/crop_calculator'
3
+
4
+ module Milton
5
+ class Thumbnail < Derivative
6
+ def process
7
+ raise "target size must be specified for resizing" unless options.has_key?(:size)
8
+
9
+ temp_dst = File.join(settings[:tempfile_path], Milton::Tempfile.from(@source.filename))
10
+ temp_src = File.join(settings[:tempfile_path], Milton::Tempfile.from(@source.filename))
11
+
12
+ @source.copy(temp_src)
13
+ image = Image.from_path(temp_src)
14
+
15
+ # TODO: this really only makes sense for processing recipes, reimplement
16
+ # once it's setup to build all derivatives then push to storage
17
+ #
18
+ # For speed, any derivatives less than 640-wide are made from a
19
+ # 640-wide version of the image (so you're not generating tiny
20
+ # thumbnails from an 8-megapixel upload)
21
+ # source = if image.width > 640 && Image.from_geometry(options[:size]).width < 640
22
+ # Thumbnail.process(@source, { :size => '640x' }, settings).file
23
+ # else
24
+ # @source
25
+ # end
26
+
27
+ if options[:crop]
28
+ crop = CropCalculator.new(image, Image.from_geometry(options[:size]))
29
+ size = crop.resizing_geometry
30
+ conversion_options = %Q(-gravity #{crop.gravity} -crop #{crop.cropping_geometry})
31
+ end
32
+
33
+ Milton.syscall!(%Q{convert #{temp_src} -geometry #{size || options[:size]} #{conversion_options || ''} +repage "#{temp_dst}"})
34
+
35
+ # TODO: raise if the store fails
36
+ file.store(temp_dst)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,64 @@
1
+ module Milton
2
+ class CropCalculator
3
+ attr_reader :original, :target
4
+
5
+ # Initializes a new CropCalculator with the two given Images.
6
+ #
7
+ # A CropCalculator is used to calculate the proper zoom/crop dimensions
8
+ # to be passed to ImageMagick's convert method in order to transform
9
+ # the original Image's dimensions into the target Image's dimensions
10
+ # with sensible zoom/cropping.
11
+ def initialize(original, target)
12
+ @original = original
13
+ @target = target
14
+ end
15
+
16
+ # Returns the geometry string to send to ImageMagick's convert -resize
17
+ # argument -- that is, the dimensions that the original Image would
18
+ # need to be resized to in order to result in the given target Image's
19
+ # dimensions with cropping.
20
+ def resizing_geometry
21
+ case
22
+ when original.wider? then "#{resized_width}x#{target.height}"
23
+ when original.square? && target.wider? then "#{target.width}x#{resized_height}"
24
+ when original.square? && !target.wider? then "#{resized_width}x#{target.height}"
25
+ else "#{target.width}x#{resized_height}"
26
+ end
27
+ end
28
+
29
+ # The geometry string to send to ImageMagick's convert -crop argument.
30
+ def cropping_geometry
31
+ "#{target.width}x#{target.height}+0+0"
32
+ end
33
+
34
+ # The gravity to use for cropping.
35
+ def gravity
36
+ original.wider? ? "center" : "north"
37
+ end
38
+
39
+ private
40
+
41
+ def resized_width
42
+ (target.height * original.width / original.height).to_i
43
+ end
44
+
45
+ def resized_height
46
+ (target.width * original.height / original.width).to_i
47
+ end
48
+
49
+ # TODO: this is the old-school cropping w/ coords, need to implement
50
+ # cropping w/ coords using the new system calls
51
+ # def crop_with_coordinates(img, x, y, size, options={})
52
+ # gravity = options[:gravity] || Magick::NorthGravity
53
+ # cropped_img = nil
54
+ # img = Magick::Image.read(img).first unless img.is_a?(Magick::Image)
55
+ # szx, szy = img.columns, img.rows
56
+ # sz = self.class.get_size_from_parameter(size)
57
+ # # logger.info "crop_with_coordinates: img.crop!(#{x}, #{y}, #{sz[0]}, #{sz[1]}, true)"
58
+ # # cropped_img = img.resize!(sz[0], sz[1]) # EEEEEK
59
+ # cropped_img = img.crop!(x, y, szx, szy, true)
60
+ # cropped_img.crop_resized!(sz[0], sz[1], gravity) # EEEEEK
61
+ # self.temp_path = write_to_temp_file(cropped_img.to_blob)
62
+ # end
63
+ end
64
+ end