jeremyboles-graffic 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/init.rb CHANGED
@@ -1,5 +1 @@
1
- require 'rmagick'
2
- require 'state_machine'
3
-
4
1
  require 'graffic'
5
- require 'graffic/aws'
@@ -0,0 +1,15 @@
1
+ # TODO: Document this better and figure out how to handle this with a module
2
+ class Graffic < ActiveRecord::Base
3
+ class << self
4
+ def class_inheritable_writer_with_default(*syms)
5
+ class_inheritable_writer_without_default(*syms)
6
+ if syms.last.is_a?(Hash) && default = syms.last.delete(:default)
7
+ syms.flatten.each do |sym|
8
+ next if sym.is_a?(Hash)
9
+ send(sym.to_s + '=', default)
10
+ end
11
+ end
12
+ end
13
+ alias_method_chain :class_inheritable_writer, :default
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module Graffic::ViewHelpers
2
+ def graffic_tag(graffic, version_or_opts = {}, opts = nil)
3
+ if graffic
4
+ if version_or_opts.is_a?(Symbol)
5
+ graffic_tag(graffic.try(version_or_opts), opts)
6
+ else
7
+ image_tag(graffic.url, version_or_opts.merge(:size => graffic.size))
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ ActionView::Base.send :include, Graffic::ViewHelpers
data/lib/graffic.rb CHANGED
@@ -1,66 +1,33 @@
1
1
  require 'rmagick'
2
- require 'state_machine'
3
2
  require 'graffic/aws'
3
+ require 'graffic/ext'
4
4
 
5
- # Graffic
6
- class Graffic < ActiveRecord::Base
7
- attr_writer :file
8
- attr_writer :image
9
- cattr_writer :bucket_name
10
- cattr_writer :queue_name
5
+ require 'graffic/view_helpers'
6
+
7
+
8
+ # Graffic is an ActiveRecord class to make dealing with Image assets more enjoyable.
9
+ # Each image is an ActiveRecord object/record and therefor can be attached to other models
10
+ # through ActiveRecord's normal has_one and has_many methods.
11
+ # A Graffic record progresses through four states: received, moved, uploaded and processed.
12
+ # Graffic is designed in a way to let slow operating states out of the request cycle, if desired.
13
+ #
14
+ class Graffic < ActiveRecord::Base
15
+ after_destroy :delete_s3_file
11
16
 
12
- belongs_to :resource, :polymorphic => true
17
+ attr_writer :file, :processor
13
18
 
14
- after_create :move
15
- after_destroy :delete_s3_file
19
+ before_validation_on_create :set_initial_state
16
20
 
17
- # We're using the state machine to keep track of what stage of photo
18
- # processing we're at. Here are the states:
19
- #
20
- # received -> moved -> uploaded -> processed
21
- #
22
- state_machine :state, :initial => :received do
23
- before_transition :to => :moved, :do => :move!
24
- before_transition :to => :uploaded, :do => :save_original!
25
- before_transition :from => :moved, :do => :upload!
26
- before_transition :to => :processed, :do => :process!
27
-
28
- after_transition :from => :moved, :do => :remove_moved_file!
29
- after_transition :to => :uploaded, :do => :queue_job!
30
- after_transition :to => :processed, :from => :uploaded, :do => :create_sizes!
31
- after_transition :to => :processed, :do => :record_dimensions!
32
-
33
- event :move do
34
- transition :to => :moved, :from => :received
35
- end
36
-
37
- event :upload do
38
- transition :to => :uploaded, :from => :moved
39
- end
40
-
41
- event :upload_unprocessed do
42
- transition :to => :processed, :from => :moved
43
- end
44
-
45
- event :process do
46
- transition :to => :processed, :from => :uploaded
47
- end
48
-
49
- # Based on which state we're at, we'll need to pull the image from a different are
50
- state :moved do
51
- # If it hasn't been moved, we'll need to pull it from the local file system
52
- def image
53
- @image ||= Magick::Image.read(tmp_file_path).first
54
- end
55
- end
56
-
57
- state :uploaded, :processed do
58
- # If it's been uploaded and procesed, grab it from S3
59
- def image
60
- @image ||= Magick::Image.from_blob(bucket.get(uploaded_file_path)).first
61
- end
62
- end
63
- end
21
+ belongs_to :resource, :polymorphic => true
22
+
23
+ class_inheritable_accessor :bucket_name, :processor
24
+ class_inheritable_accessor :format, :default => 'png'
25
+ class_inheritable_accessor :process_queue_name, :default => 'graffic_process'
26
+ class_inheritable_accessor :upload_queue_name, :default => 'graffic_upload'
27
+ class_inheritable_accessor :tmp_dir, :default => RAILS_ROOT + '/tmp/graffics'
28
+ class_inheritable_hash :versions
29
+
30
+ validate_on_create :file_was_given
64
31
 
65
32
  class << self
66
33
  # Returns the bucket for the model
@@ -68,213 +35,274 @@ class Graffic < ActiveRecord::Base
68
35
  @bucket ||= Graffic::Aws.s3.bucket(bucket_name, true, 'public-read')
69
36
  end
70
37
 
71
- # Change the bucket name from the default
72
- def bucket_name(name = nil)
73
- @@bucket_name = name unless name == nil
74
- @@bucket_name
75
- end
76
-
77
- # Upload all of the files that have been moved. This will only work on
78
- # local files.
79
- # TODO: Make this work only on file system that the file was used on
80
- def handle_moved!
81
- image = first(:conditions => { :state => 'moved' })
82
- return if image.nil?
83
- image.upload
38
+ def create_tmp_dir
39
+ FileUtils.mkdir(tmp_dir) unless File.exists?(tmp_dir)
84
40
  end
85
41
 
86
- # Process all of the the uploaded files that are in the queue
87
- def handle_uploaded!
88
- message = queue.pop
89
- return if message.nil?
90
- image = find(message.body)
91
- image.process
92
- message.body
93
- rescue
94
- true
42
+ # Handle the first message in toe process queue
43
+ def handle_top_in_process_queue!
44
+ if message = process_queue.receive
45
+ data = YAML.load(message.to_s)
46
+ return unless record = find(data[:id])
47
+ record.process!
48
+ message.delete
49
+ end
95
50
  end
96
51
 
97
- def inherited(child)
98
- child.has_one(:original, :class_name => 'Graffic', :as => :resource, :dependent => :destroy, :conditions => { :name => 'original' })
99
- super
52
+ # Handles the first message in the upload queue
53
+ def handle_top_in_upload_queue!
54
+ if message = upload_queue.receive
55
+ data = YAML.load(message.to_s)
56
+ return if data[:hostname] != `hostname`.strip
57
+ return unless record = find(data[:id])
58
+ record.upload!
59
+ message.delete
60
+ end
100
61
  end
101
62
 
102
63
  def process(&block)
103
- if block_given?
104
- @process = block
105
- else
106
- return @process
107
- end
64
+ self.processor = block if block_given?
108
65
  end
109
66
 
110
- # Change the queue name from the default
111
- def queue_name(name = nil)
112
- @@queue_name = name unless name == nil
113
- @@queue_name || 'graffic'
67
+ def inherited(subclass)
68
+ subclass.has_one(:original, :class_name => 'Graffic', :as => :resource, :dependent => :destroy, :conditions => { :name => 'original' })
69
+ super
114
70
  end
115
71
 
116
- # Return the model's queue
117
- def queue
118
- @queue ||= Graffic::Aws.sqs.queue(queue_name, true)
72
+ # The queue for processing images
73
+ def process_queue
74
+ @process_queue ||= Graffic::Aws.sqs.queue(process_queue_name, true)
119
75
  end
120
76
 
121
- # Create a size of the graphic.
122
- def size(name, size={})
123
- size[:format] ||= :png
124
- size.assert_valid_keys(:width, :height, :format)
125
-
126
- @sizes ||= {}
127
- @sizes[name] = size
128
-
129
- has_one(name, :class_name => 'Graffic', :as => :resource, :dependent => :destroy, :conditions => { :name => name.to_s })
77
+ def size(name, size = {})
78
+ size.assert_valid_keys(:width, :height)
79
+ version(name) {|img| img.crop_resized(size[:width], size[:height])}
130
80
  end
131
81
 
132
- # Returns all of the version names for the mode
133
- def sizes
134
- @sizes ||= {}
82
+ # The queue for uploading images
83
+ def upload_queue
84
+ @upload_queue ||= Graffic::Aws.sqs.queue(upload_queue_name, true)
135
85
  end
136
86
 
137
- # Set the image format
138
- def format(format = nil)
139
- @format = format unless format.nil?
140
- @format ||= :png
87
+ def version(name, &block)
88
+ self.versions ||= {}
89
+ if block_given?
90
+ self.versions[name] = block || nil
91
+ has_one(name, :class_name => 'Graffic', :as => :resource, :dependent => :destroy, :conditions => { :name => name.to_s })
92
+ end
93
+ end
94
+ end
95
+
96
+ # The format of the image
97
+ def format
98
+ attributes['format'] || self.class.format
99
+ end
100
+
101
+ # Move the file to the temporary directory
102
+ def move!
103
+ move_without_queue!
104
+ queue_for_upload
105
+ end
106
+
107
+ # Move the file to the temporary directory
108
+ def move_without_queue!
109
+ logger.debug("***** Graffic[#{self.id}](#{self.name})#move!")
110
+ if @file.is_a?(Tempfile)
111
+ @file.write(tmp_file_path)
112
+ change_state('moved')
113
+ elsif @file.is_a?(String)
114
+ FileUtils.cp(@file, tmp_file_path)
115
+ change_state('moved')
116
+ elsif @file.is_a?(Magick::Image)
117
+ @image = @file
118
+ upload_without_queue!
119
+ change_state('uploaded')
120
+ end
121
+ end
122
+
123
+ # Process the image
124
+ def process!
125
+ process_without_verions!
126
+ process_versions
127
+ end
128
+
129
+ # Process the image without the versions
130
+ def process_without_verions!
131
+ logger.debug("***** Graffic[#{self.id}](#{self.name})#process!")
132
+ run_processors
133
+ record_image_dimensions_and_format
134
+ upload_image
135
+ change_state('processed')
136
+ end
137
+
138
+ # Returns the processor for the instance
139
+ def processor
140
+ @processor || self.class.processor
141
+ end
142
+
143
+ # Move the file if it saved successfully
144
+ def save_and_move
145
+ move! if status = save
146
+ status
147
+ end
148
+
149
+ # Save the file and process it immediately. Does to use queues.
150
+ def save_and_process
151
+ if status = save
152
+ move_without_queue!
153
+ upload_without_queue!
154
+ process!
141
155
  end
156
+ status
142
157
  end
143
158
 
144
- # Returns a size string
159
+ # Returns a size string. Good for RMagick and image_tag :size
145
160
  def size
146
161
  "#{width}x#{height}"
147
162
  end
148
163
 
164
+ # Upload the file
165
+ def upload!
166
+ upload_without_queue!
167
+ queue_for_processing
168
+ end
169
+
170
+ # Upload the file
171
+ def upload_without_queue!
172
+ logger.debug("***** Graffic[#{self.id}](#{self.name})#upload!")
173
+ upload_image
174
+ save_original
175
+ remove_tmp_file
176
+ change_state('uploaded')
177
+ end
178
+
149
179
  # Return the url for displaying the image
150
180
  def url
151
- key.public_link
181
+ self.s3_key.public_link
152
182
  end
153
183
 
154
- private
184
+ protected
185
+
155
186
  # Connivence method for getting the bucket
156
187
  def bucket
157
188
  self.class.bucket
158
189
  end
159
-
160
- def create_sizes!
161
- self.class.sizes.each do |name, size|
162
- logger.debug("***** Sizing: #{name}")
163
- file_name = "#{tmp_file_path}.#{name}.#{image_extension(size[:format])}"
164
-
165
- img = image.crop_resized(size[:width], size[:height])
166
- img.write(file_name)
167
-
168
- i = Graffic.create(:file => file_name, :format => size[:format].to_s, :name => name.to_s)
169
- update_attribute(name, i)
170
- i.upload_unprocessed
171
-
172
- FileUtils.rm(file_name)
173
- end
190
+
191
+ # Save the state without running all the callbacks
192
+ def change_state(state)
193
+ logger.debug("***** Graffic[#{self.id}](#{self.name}): Changing state to: #{state}")
194
+ self.state = state
195
+ save(false)
174
196
  end
175
197
 
176
- # Deletes the file from S3
177
198
  def delete_s3_file
178
- key.delete
199
+ self.s3_key.delete
179
200
  end
180
201
 
181
- # The formate of the image
182
- def format
183
- attributes['format'] || self.class.format
202
+ # Make sure a file was given
203
+ def file_was_given
204
+ self.errors.add(:file, 'not included. You need a file when creating.') if @file.nil?
184
205
  end
185
206
 
186
- # Returns true if the file has versions
187
- def has_sizes?
188
- !self.class.sizes.empty?
207
+ def image
208
+ @image ||= case self.state
209
+ when 'moved': Magick::Image.read(tmp_file_path).first
210
+ when 'uploaded', 'processed': Magick::Image.from_blob(bucket.get(uploaded_file_path)).first
211
+ end
189
212
  end
190
213
 
191
- def image_extension(atype = nil)
214
+ # Return the file extension based on the type
215
+ def extension(atype = nil)
192
216
  (atype || format).to_s
193
217
  end
194
218
 
195
- # Return the S3 key for the record
196
- def key
197
- @s3_key ||= bucket.key(uploaded_file_path)
198
- end
199
-
200
- # If the file is a Tempfile, we'll need to move to the app's tmp directory so
201
- # we can insure that it is retrained until we can upload it
202
- # If its a S3 Key, we'll write that file's date to our tmp directory
203
- def move!
204
- if @file.is_a?(Tempfile)
205
- @file.write(tmp_file_path)
206
- elsif @file.is_a?(String)
207
- FileUtils.cp(@file, tmp_file_path)
208
- end
219
+ def process_queue
220
+ self.class.process_queue
209
221
  end
210
222
 
211
- # Process the image
212
- def process!
213
- unless self.class.process.nil?
214
- @image = self.class.process.call(image)
215
- raise 'You need to return an image' unless @image.is_a?(Magick::Image)
216
- upload!
223
+ def process_versions
224
+ unless self.versions.blank?
225
+ self.versions.each do |version, processor|
226
+ logger.debug("***** Graffic[#{self.id}](#{self.name}): Processing version: #{version}")
227
+ g = Graffic.create(:file => self.image, :name => version.to_s)
228
+ g.processor = processor unless processor.nil?
229
+ g.save_and_process
230
+ self.update_attribute(version, g)
231
+ end
217
232
  end
218
233
  end
219
234
 
220
- # Connivence method for getting the queue
221
- def queue
222
- self.class.queue
235
+ def queue_for_upload
236
+ self.upload_queue.push({ :id => self.id, :hostname => `hostname`.strip }.to_yaml)
223
237
  end
224
238
 
225
- # Add a job to the queue
226
- def queue_job!
227
- logger.debug("***** Graffic(#{self.id})#queue_job!")
228
- queue.push(self.id)
239
+ def queue_for_processing
240
+ self.process_queue.push({ :id => self.id }.to_yaml)
229
241
  end
230
242
 
231
- # Save the image's width and height to the database
232
- def record_dimensions!
233
- logger.debug("***** Graffic(#{self.id})#record_dimensions!")
234
- self.update_attributes(:height => image.rows, :width => image.columns)
243
+ def record_image_dimensions_and_format
244
+ self.height, self.width, self.format = image.rows, image.columns, self.format
245
+ save(false)
235
246
  end
236
247
 
237
- # Remove the temp file in the app's temp director
238
- def remove_moved_file!
239
- logger.debug("***** Graffic(#{self.id})#remove_moved_file!")
248
+ def remove_tmp_file
240
249
  FileUtils.rm(tmp_file_path) if File.exists?(tmp_file_path)
241
250
  end
242
251
 
243
252
  # Returns a RMagick constant for the type of image
244
253
  def rmagick_type(atype = nil)
245
- return case (atype || format).to_sym
254
+ return case (atype || self.format).to_sym
246
255
  when :gif then Magick::LZWCompression
247
256
  when :jpg then Magick::JPEGCompression
248
257
  when :png then Magick::ZipCompression
249
258
  end
250
259
  end
251
260
 
252
- # Uploads an untouched original
253
- def save_original!
254
- logger.debug("***** Graffic(#{self.id})#save_original!")
261
+ def run_processors
262
+ logger.debug("***** Graffic[#{self.id}](#{self.name}): Running processor")
263
+ unless self.processor.blank?
264
+ @image = processor.call(image)
265
+ raise 'You need to return an image' unless @image.is_a?(Magick::Image)
266
+ end
267
+ end
268
+
269
+ def s3_key
270
+ @s3_key ||= bucket.key(uploaded_file_path)
271
+ end
272
+
273
+ def save_original
255
274
  if respond_to?(:original)
256
- i = Graffic.new(:file => tmp_file_path, :name => 'original')
257
- update_attribute(:original, i)
258
- i.upload_unprocessed
275
+ logger.debug("***** Graffic[#{self.id}](#{self.name}): Saving Original")
276
+ g = Graffic.new(:file => tmp_file_path, :name => 'original')
277
+ g.save_and_process
278
+ self.update_attribute(:original, g)
259
279
  end
260
280
  end
281
+
282
+ # If we got a new file, we need to start over with the state
283
+ def set_initial_state
284
+ self.state = 'received' unless @file.nil?
285
+ end
261
286
 
262
- # Returns the path to the file in the app's tmp directory
263
287
  def tmp_file_path
264
- RAILS_ROOT + "/tmp/images/#{id}.tmp"
288
+ self.tmp_dir + "/#{id}.tmp"
265
289
  end
266
290
 
267
- # Upload the file to S3
268
- def upload!
269
- logger.debug("***** Graffic(#{self.id})#upload!")
270
- t = rmagick_type
291
+ # Return the path on S3 for the file (the key name, essentially)
292
+ def uploaded_file_path
293
+ "#{self.class.name.tableize}/#{id}.#{extension}"
294
+ end
295
+
296
+ # Upload the image
297
+ def upload_image
298
+ t = self.rmagick_type
271
299
  data = image.to_blob { |i| i.compression = t }
272
300
  bucket.put(uploaded_file_path, data, {}, 'public-read')
273
301
  end
274
302
 
275
- # Return the path on S3 for the file (the key name, essentially)
276
- def uploaded_file_path
277
- "#{self.class.name.tableize}/#{id}.#{image_extension}"
303
+ def upload_queue
304
+ self.class.upload_queue
278
305
  end
279
-
306
+
307
+ create_tmp_dir
280
308
  end # Graffic
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeremyboles-graffic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Boles
@@ -9,19 +9,9 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-16 00:00:00 -07:00
12
+ date: 2009-03-18 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: state_machine
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
20
- requirements:
21
- - - "="
22
- - !ruby/object:Gem::Version
23
- version: 0.6.3
24
- version:
25
15
  - !ruby/object:Gem::Dependency
26
16
  name: right_aws
27
17
  type: :runtime
@@ -62,6 +52,8 @@ files:
62
52
  - init.rb
63
53
  - lib/graffic.rb
64
54
  - lib/graffic/aws.rb
55
+ - lib/graffic/ext.rb
56
+ - lib/graffic/view_helpers.rb
65
57
  - tasks/graffic_tasks.rake
66
58
  - test/graffic_test.rb
67
59
  - test/test_helper.rb