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 +0 -4
- data/lib/graffic/ext.rb +15 -0
- data/lib/graffic/view_helpers.rb +13 -0
- data/lib/graffic.rb +216 -188
- metadata +4 -12
data/init.rb
CHANGED
data/lib/graffic/ext.rb
ADDED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
17
|
+
attr_writer :file, :processor
|
13
18
|
|
14
|
-
|
15
|
-
after_destroy :delete_s3_file
|
19
|
+
before_validation_on_create :set_initial_state
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
72
|
-
|
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
|
-
#
|
87
|
-
def
|
88
|
-
message =
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
#
|
117
|
-
def
|
118
|
-
@
|
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
|
-
|
122
|
-
|
123
|
-
size[:
|
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
|
-
#
|
133
|
-
def
|
134
|
-
@
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
181
|
+
self.s3_key.public_link
|
152
182
|
end
|
153
183
|
|
154
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
199
|
+
self.s3_key.delete
|
179
200
|
end
|
180
201
|
|
181
|
-
#
|
182
|
-
def
|
183
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
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
|
-
|
196
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
221
|
-
|
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
|
-
|
226
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
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
|
-
|
257
|
-
|
258
|
-
|
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
|
-
|
288
|
+
self.tmp_dir + "/#{id}.tmp"
|
265
289
|
end
|
266
290
|
|
267
|
-
#
|
268
|
-
def
|
269
|
-
|
270
|
-
|
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
|
-
|
276
|
-
|
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.
|
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-
|
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
|