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