jeremyboles-graffic 0.1.2 → 0.2.0

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.
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