bulldog 0.0.1
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/.gitignore +2 -0
- data/DESCRIPTION.txt +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/bulldog.gemspec +157 -0
- data/lib/bulldog.rb +95 -0
- data/lib/bulldog/attachment.rb +49 -0
- data/lib/bulldog/attachment/base.rb +167 -0
- data/lib/bulldog/attachment/has_dimensions.rb +94 -0
- data/lib/bulldog/attachment/image.rb +63 -0
- data/lib/bulldog/attachment/maybe.rb +229 -0
- data/lib/bulldog/attachment/none.rb +37 -0
- data/lib/bulldog/attachment/pdf.rb +63 -0
- data/lib/bulldog/attachment/unknown.rb +11 -0
- data/lib/bulldog/attachment/video.rb +143 -0
- data/lib/bulldog/error.rb +5 -0
- data/lib/bulldog/has_attachment.rb +214 -0
- data/lib/bulldog/interpolation.rb +73 -0
- data/lib/bulldog/missing_file.rb +12 -0
- data/lib/bulldog/processor.rb +5 -0
- data/lib/bulldog/processor/argument_tree.rb +116 -0
- data/lib/bulldog/processor/base.rb +124 -0
- data/lib/bulldog/processor/ffmpeg.rb +172 -0
- data/lib/bulldog/processor/image_magick.rb +134 -0
- data/lib/bulldog/processor/one_shot.rb +19 -0
- data/lib/bulldog/reflection.rb +234 -0
- data/lib/bulldog/saved_file.rb +19 -0
- data/lib/bulldog/stream.rb +186 -0
- data/lib/bulldog/style.rb +38 -0
- data/lib/bulldog/style_set.rb +101 -0
- data/lib/bulldog/tempfile.rb +28 -0
- data/lib/bulldog/util.rb +92 -0
- data/lib/bulldog/validations.rb +68 -0
- data/lib/bulldog/vector2.rb +18 -0
- data/rails/init.rb +9 -0
- data/script/console +8 -0
- data/spec/data/empty.txt +0 -0
- data/spec/data/test.jpg +0 -0
- data/spec/data/test.mov +0 -0
- data/spec/data/test.pdf +0 -0
- data/spec/data/test.png +0 -0
- data/spec/data/test2.jpg +0 -0
- data/spec/helpers/image_creation.rb +8 -0
- data/spec/helpers/temporary_directory.rb +25 -0
- data/spec/helpers/temporary_models.rb +76 -0
- data/spec/helpers/temporary_values.rb +102 -0
- data/spec/helpers/test_upload_files.rb +108 -0
- data/spec/helpers/time_travel.rb +20 -0
- data/spec/integration/data/test.jpg +0 -0
- data/spec/integration/lifecycle_hooks_spec.rb +213 -0
- data/spec/integration/processing_image_attachments.rb +72 -0
- data/spec/integration/processing_video_attachments_spec.rb +82 -0
- data/spec/integration/saving_an_attachment_spec.rb +31 -0
- data/spec/matchers/file_operations.rb +159 -0
- data/spec/spec_helper.rb +76 -0
- data/spec/unit/attachment/base_spec.rb +311 -0
- data/spec/unit/attachment/image_spec.rb +128 -0
- data/spec/unit/attachment/maybe_spec.rb +126 -0
- data/spec/unit/attachment/pdf_spec.rb +137 -0
- data/spec/unit/attachment/video_spec.rb +176 -0
- data/spec/unit/attachment_spec.rb +61 -0
- data/spec/unit/has_attachment_spec.rb +700 -0
- data/spec/unit/interpolation_spec.rb +108 -0
- data/spec/unit/processor/argument_tree_spec.rb +159 -0
- data/spec/unit/processor/ffmpeg_spec.rb +467 -0
- data/spec/unit/processor/image_magick_spec.rb +260 -0
- data/spec/unit/processor/one_shot_spec.rb +70 -0
- data/spec/unit/reflection_spec.rb +338 -0
- data/spec/unit/stream_spec.rb +234 -0
- data/spec/unit/style_set_spec.rb +44 -0
- data/spec/unit/style_spec.rb +51 -0
- data/spec/unit/validations_spec.rb +491 -0
- data/spec/unit/vector2_spec.rb +27 -0
- data/tasks/bulldog_tasks.rake +4 -0
- metadata +193 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Bulldog
|
2
|
+
module Processor
|
3
|
+
class OneShot < Base
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@styles.clear
|
7
|
+
end
|
8
|
+
|
9
|
+
def process(*args, &block)
|
10
|
+
@style = nil
|
11
|
+
process_style(*args, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def output_file(style_name)
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
module Bulldog
|
2
|
+
class Reflection
|
3
|
+
def initialize(model_class, name)
|
4
|
+
@model_class = model_class
|
5
|
+
@name = name
|
6
|
+
|
7
|
+
@path_template = nil
|
8
|
+
@url_template = nil
|
9
|
+
@styles = StyleSet.new
|
10
|
+
@default_style = :original
|
11
|
+
@stored_attributes = {}
|
12
|
+
@events = Hash.new{|h,k| h[k] = []}
|
13
|
+
@type = nil
|
14
|
+
@type_detector = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize_copy(other)
|
18
|
+
super
|
19
|
+
instance_variables.each do |name|
|
20
|
+
value = instance_variable_get(name)
|
21
|
+
value = value.clone if value.duplicable?
|
22
|
+
instance_variable_set(name, value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_accessor :model_class, :name, :path_template, :url_template, :styles, :events, :stored_attributes, :type, :type_detector
|
27
|
+
attr_writer :default_style, :path_template, :url_template
|
28
|
+
|
29
|
+
#
|
30
|
+
# Append the given block to this attachment's configuration.
|
31
|
+
#
|
32
|
+
# Using this, you may specialize an attachment's configuration in
|
33
|
+
# a piecemeal fashion; for subclassing, for example.
|
34
|
+
#
|
35
|
+
def configure(&block)
|
36
|
+
Configuration.configure(self, &block) if block
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_style
|
40
|
+
styles[@default_style] or
|
41
|
+
raise Error, "invalid default_style: #{@default_style.inspect}"
|
42
|
+
@default_style
|
43
|
+
end
|
44
|
+
|
45
|
+
def path_template
|
46
|
+
@path_template || Bulldog.default_path_template || File.join(':public_path', url_template)
|
47
|
+
end
|
48
|
+
|
49
|
+
def url_template
|
50
|
+
@url_template || Bulldog.default_url_template
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Return the column name to use for the named storable attribute.
|
55
|
+
#
|
56
|
+
def column_name_for_stored_attribute(attribute)
|
57
|
+
if stored_attributes.fetch(attribute, :not_nil).nil?
|
58
|
+
nil
|
59
|
+
elsif (value = stored_attributes[attribute])
|
60
|
+
value
|
61
|
+
else
|
62
|
+
default_column = "#{name}_#{attribute}"
|
63
|
+
column_exists = model_class.columns_hash.key?(default_column)
|
64
|
+
column_exists ? default_column.to_sym : nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.named_type_detectors
|
69
|
+
@named_type_detectors ||= {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.to_detect_type_by(name, &block)
|
73
|
+
named_type_detectors[name] = block
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.reset_type_detectors
|
77
|
+
named_type_detectors.clear
|
78
|
+
end
|
79
|
+
|
80
|
+
to_detect_type_by :default do |record, name, stream|
|
81
|
+
if stream
|
82
|
+
case stream.content_type
|
83
|
+
when %r'\Aimage/'
|
84
|
+
:image
|
85
|
+
when %r'\Avideo/'
|
86
|
+
:video
|
87
|
+
when %r'\Aapplication/pdf'
|
88
|
+
:pdf
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Return the attachment type to use for the given record and
|
95
|
+
# stream.
|
96
|
+
#
|
97
|
+
def detect_attachment_type(record, stream)
|
98
|
+
return type if type
|
99
|
+
detector = type_detector || :default
|
100
|
+
if detector.is_a?(Symbol)
|
101
|
+
detector = self.class.named_type_detectors[detector]
|
102
|
+
end
|
103
|
+
detector.call(record, name, stream)
|
104
|
+
end
|
105
|
+
|
106
|
+
class Configuration
|
107
|
+
def self.configure(reflection, &block)
|
108
|
+
new(reflection).instance_eval(&block)
|
109
|
+
end
|
110
|
+
|
111
|
+
def initialize(reflection)
|
112
|
+
@reflection = reflection
|
113
|
+
end
|
114
|
+
|
115
|
+
def path(path_template)
|
116
|
+
@reflection.path_template = path_template
|
117
|
+
end
|
118
|
+
|
119
|
+
def url(url_template)
|
120
|
+
@reflection.url_template = url_template
|
121
|
+
end
|
122
|
+
|
123
|
+
def style(name, attributes={})
|
124
|
+
@reflection.styles << Style.new(name, attributes)
|
125
|
+
end
|
126
|
+
|
127
|
+
def default_style(name)
|
128
|
+
@reflection.default_style = name
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Register the callback to fire for the given types of
|
133
|
+
# attachments.
|
134
|
+
#
|
135
|
+
# Options:
|
136
|
+
#
|
137
|
+
# * :types - A list of attachment types to process on. If
|
138
|
+
# nil, process on any type.
|
139
|
+
# * :on - The name of the event to run the processor on.
|
140
|
+
# * :after - Same as prepending 'after_' to the given event.
|
141
|
+
# * :before - Same as prepending 'before_' to the given event.
|
142
|
+
# * :with - Use the given processor type. If nil (the
|
143
|
+
# default), use the default type for the attachment.
|
144
|
+
#
|
145
|
+
def process(options={}, &callback)
|
146
|
+
event_name = event_name(options)
|
147
|
+
types = Array(options[:types]) if options[:types]
|
148
|
+
@reflection.events[event_name] << Event.new(:processor_type => options[:with],
|
149
|
+
:attachment_types => types,
|
150
|
+
:styles => options[:styles],
|
151
|
+
:callback => callback)
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_once(*types, &callback)
|
155
|
+
options = types.extract_options!
|
156
|
+
options[:with] and
|
157
|
+
raise ArgumentError, "cannot specify a processor (:with option) with #process_once"
|
158
|
+
options[:styles] and
|
159
|
+
raise ArgumentError, "no :styles available for #process_once"
|
160
|
+
options[:with] = :one_shot
|
161
|
+
types << options
|
162
|
+
process(*types, &callback)
|
163
|
+
end
|
164
|
+
|
165
|
+
def store_attributes(*args)
|
166
|
+
stored_attributes = args.extract_options!.symbolize_keys
|
167
|
+
args.each do |attribute|
|
168
|
+
stored_attributes[attribute] = :"#{@reflection.name}_#{attribute}"
|
169
|
+
end
|
170
|
+
@reflection.stored_attributes = stored_attributes
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Always use the given attachment type for this attachment.
|
175
|
+
#
|
176
|
+
# This is equivalent to:
|
177
|
+
#
|
178
|
+
# detect_type_by do
|
179
|
+
# type if stream
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
def type(type)
|
183
|
+
@reflection.type = type
|
184
|
+
@reflection.type_detector = nil
|
185
|
+
end
|
186
|
+
|
187
|
+
#
|
188
|
+
# Specify a procedure to run to determine the type of the
|
189
|
+
# attachment.
|
190
|
+
#
|
191
|
+
# Pass either:
|
192
|
+
#
|
193
|
+
# * A symbol argument, which names a named type detector. Use
|
194
|
+
# +Bulldog.to_detect_type_by+ to register custom named type
|
195
|
+
# detectors.
|
196
|
+
#
|
197
|
+
# * A block, which takes the record, attribute name, and
|
198
|
+
# Stream being assigned, and returns the attachment type to
|
199
|
+
# use as a Symbol.
|
200
|
+
#
|
201
|
+
# * A callable object, e.g. a Proc or BoundMethod, which has
|
202
|
+
# the same signature as the block above.
|
203
|
+
#
|
204
|
+
def detect_type_by(detector=nil, &block)
|
205
|
+
detector && block and
|
206
|
+
raise ArgumentError, "cannot pass argument and a block"
|
207
|
+
@reflection.type = nil
|
208
|
+
@reflection.type_detector = detector || block
|
209
|
+
end
|
210
|
+
|
211
|
+
private # -----------------------------------------------------
|
212
|
+
|
213
|
+
def event_name(options)
|
214
|
+
name = options[:on] and
|
215
|
+
return name
|
216
|
+
name = options[:after] and
|
217
|
+
return :"after_#{name}"
|
218
|
+
name = options[:before] and
|
219
|
+
return :"before_#{name}"
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class Event
|
225
|
+
def initialize(attributes={})
|
226
|
+
attributes.each do |name, value|
|
227
|
+
send("#{name}=", value)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
attr_accessor :processor_type, :attachment_types, :styles, :callback
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Bulldog
|
2
|
+
class SavedFile
|
3
|
+
def initialize(path, options={})
|
4
|
+
@path = path
|
5
|
+
@file_name = options[:file_name]
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :path
|
9
|
+
|
10
|
+
#
|
11
|
+
# The original file name as it was uploaded, if any.
|
12
|
+
#
|
13
|
+
attr_reader :file_name
|
14
|
+
|
15
|
+
def size
|
16
|
+
File.size(path)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module Bulldog
|
2
|
+
#
|
3
|
+
# Gives IO, StringIO, Tempfile, and SavedFile a common interface.
|
4
|
+
#
|
5
|
+
# In particular, this takes care of writing it to a file so external
|
6
|
+
# programs may be called on it, while keeping the number of file
|
7
|
+
# writes to a minimum.
|
8
|
+
#
|
9
|
+
module Stream
|
10
|
+
def self.new(object)
|
11
|
+
klass =
|
12
|
+
case object
|
13
|
+
when ::Tempfile
|
14
|
+
ForTempfile
|
15
|
+
when File
|
16
|
+
ForFile
|
17
|
+
when SavedFile
|
18
|
+
ForSavedFile
|
19
|
+
when MissingFile
|
20
|
+
ForMissingFile
|
21
|
+
when StringIO
|
22
|
+
ForStringIO
|
23
|
+
when IO
|
24
|
+
ForIO
|
25
|
+
end
|
26
|
+
klass.new(object)
|
27
|
+
end
|
28
|
+
|
29
|
+
class Base
|
30
|
+
def initialize(target)
|
31
|
+
@target = target
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# The underlying object.
|
36
|
+
#
|
37
|
+
attr_reader :target
|
38
|
+
|
39
|
+
#
|
40
|
+
# Return the path of a file where the content can be found.
|
41
|
+
#
|
42
|
+
def path
|
43
|
+
@target.path
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Return true if this stream represents a missing file.
|
48
|
+
#
|
49
|
+
def missing?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Return the original file name of the underlying object. This
|
55
|
+
# is the basename of the file as the user uploaded it (for an
|
56
|
+
# uploaded file), or the file on the filesystem (for a File
|
57
|
+
# object). For other IO objects, return nil.
|
58
|
+
#
|
59
|
+
def file_name
|
60
|
+
if @target.respond_to?(:original_path)
|
61
|
+
@target.original_path
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Return the size of the content.
|
69
|
+
#
|
70
|
+
def size
|
71
|
+
File.size(path)
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Return the mime-type of the content.
|
76
|
+
#
|
77
|
+
def content_type
|
78
|
+
@content_type ||= `file --brief --mime #{path}`.strip
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Write the content to the given path.
|
83
|
+
#
|
84
|
+
def write_to(path)
|
85
|
+
src, dst = self.path, path
|
86
|
+
unless src == dst
|
87
|
+
FileUtils.mkdir_p File.dirname(path)
|
88
|
+
FileUtils.cp src, dst
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ForTempfile < Base
|
94
|
+
def initialize(target)
|
95
|
+
super
|
96
|
+
@path = target.path or
|
97
|
+
raise ArgumentError, "Tempfile is closed - cannot retrieve information"
|
98
|
+
end
|
99
|
+
|
100
|
+
def path
|
101
|
+
@target.flush unless @target.closed?
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
def size
|
106
|
+
# Tempfile#size returns nil when closed.
|
107
|
+
@target.flush unless @target.closed?
|
108
|
+
File.size(@target.path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class ForFile < Base
|
113
|
+
def path
|
114
|
+
@target.flush unless @target.closed? rescue IOError # not open for writing
|
115
|
+
super
|
116
|
+
end
|
117
|
+
|
118
|
+
def file_name
|
119
|
+
super || File.basename(@target.path)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class ForSavedFile < Base
|
124
|
+
delegate :file_name, :to => :target
|
125
|
+
end
|
126
|
+
|
127
|
+
class ForMissingFile < Base
|
128
|
+
def missing?
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
delegate :file_name, :to => :target
|
133
|
+
|
134
|
+
def content_type
|
135
|
+
@target.content_type || super
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class ForIO < Base
|
140
|
+
def path
|
141
|
+
return @path if @path
|
142
|
+
# Keep extension if it's available. Some commands may use the
|
143
|
+
# file extension of the input file, for example.
|
144
|
+
tempfile_name = 'bulldog'
|
145
|
+
if @target.respond_to?(:original_path) && @target.original_path
|
146
|
+
tempfile_name = [tempfile_name, File.extname(@target.original_path)]
|
147
|
+
end
|
148
|
+
Tempfile.open(tempfile_name) do |file|
|
149
|
+
write_to_io(file)
|
150
|
+
@path = file.path
|
151
|
+
|
152
|
+
# Don't let the tempfile be GC'd until the stream is, as the
|
153
|
+
# tempfile's finalizer deletes the file.
|
154
|
+
@tempfile = file
|
155
|
+
end
|
156
|
+
@path
|
157
|
+
end
|
158
|
+
|
159
|
+
def write_to(path)
|
160
|
+
if @path
|
161
|
+
super
|
162
|
+
else
|
163
|
+
open(path, 'w') do |file|
|
164
|
+
write_to_io(file)
|
165
|
+
@path = file.path
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private # -----------------------------------------------------
|
171
|
+
|
172
|
+
BLOCK_SIZE = 64*1024
|
173
|
+
def write_to_io(io)
|
174
|
+
target.rewind rescue nil # not rewindable
|
175
|
+
buffer = ""
|
176
|
+
while target.read(BLOCK_SIZE, buffer)
|
177
|
+
io.write(buffer)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class ForStringIO < ForIO
|
183
|
+
delegate :size, :to => :target
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|