bulldog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/.gitignore +2 -0
  2. data/DESCRIPTION.txt +3 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +64 -0
  6. data/VERSION +1 -0
  7. data/bulldog.gemspec +157 -0
  8. data/lib/bulldog.rb +95 -0
  9. data/lib/bulldog/attachment.rb +49 -0
  10. data/lib/bulldog/attachment/base.rb +167 -0
  11. data/lib/bulldog/attachment/has_dimensions.rb +94 -0
  12. data/lib/bulldog/attachment/image.rb +63 -0
  13. data/lib/bulldog/attachment/maybe.rb +229 -0
  14. data/lib/bulldog/attachment/none.rb +37 -0
  15. data/lib/bulldog/attachment/pdf.rb +63 -0
  16. data/lib/bulldog/attachment/unknown.rb +11 -0
  17. data/lib/bulldog/attachment/video.rb +143 -0
  18. data/lib/bulldog/error.rb +5 -0
  19. data/lib/bulldog/has_attachment.rb +214 -0
  20. data/lib/bulldog/interpolation.rb +73 -0
  21. data/lib/bulldog/missing_file.rb +12 -0
  22. data/lib/bulldog/processor.rb +5 -0
  23. data/lib/bulldog/processor/argument_tree.rb +116 -0
  24. data/lib/bulldog/processor/base.rb +124 -0
  25. data/lib/bulldog/processor/ffmpeg.rb +172 -0
  26. data/lib/bulldog/processor/image_magick.rb +134 -0
  27. data/lib/bulldog/processor/one_shot.rb +19 -0
  28. data/lib/bulldog/reflection.rb +234 -0
  29. data/lib/bulldog/saved_file.rb +19 -0
  30. data/lib/bulldog/stream.rb +186 -0
  31. data/lib/bulldog/style.rb +38 -0
  32. data/lib/bulldog/style_set.rb +101 -0
  33. data/lib/bulldog/tempfile.rb +28 -0
  34. data/lib/bulldog/util.rb +92 -0
  35. data/lib/bulldog/validations.rb +68 -0
  36. data/lib/bulldog/vector2.rb +18 -0
  37. data/rails/init.rb +9 -0
  38. data/script/console +8 -0
  39. data/spec/data/empty.txt +0 -0
  40. data/spec/data/test.jpg +0 -0
  41. data/spec/data/test.mov +0 -0
  42. data/spec/data/test.pdf +0 -0
  43. data/spec/data/test.png +0 -0
  44. data/spec/data/test2.jpg +0 -0
  45. data/spec/helpers/image_creation.rb +8 -0
  46. data/spec/helpers/temporary_directory.rb +25 -0
  47. data/spec/helpers/temporary_models.rb +76 -0
  48. data/spec/helpers/temporary_values.rb +102 -0
  49. data/spec/helpers/test_upload_files.rb +108 -0
  50. data/spec/helpers/time_travel.rb +20 -0
  51. data/spec/integration/data/test.jpg +0 -0
  52. data/spec/integration/lifecycle_hooks_spec.rb +213 -0
  53. data/spec/integration/processing_image_attachments.rb +72 -0
  54. data/spec/integration/processing_video_attachments_spec.rb +82 -0
  55. data/spec/integration/saving_an_attachment_spec.rb +31 -0
  56. data/spec/matchers/file_operations.rb +159 -0
  57. data/spec/spec_helper.rb +76 -0
  58. data/spec/unit/attachment/base_spec.rb +311 -0
  59. data/spec/unit/attachment/image_spec.rb +128 -0
  60. data/spec/unit/attachment/maybe_spec.rb +126 -0
  61. data/spec/unit/attachment/pdf_spec.rb +137 -0
  62. data/spec/unit/attachment/video_spec.rb +176 -0
  63. data/spec/unit/attachment_spec.rb +61 -0
  64. data/spec/unit/has_attachment_spec.rb +700 -0
  65. data/spec/unit/interpolation_spec.rb +108 -0
  66. data/spec/unit/processor/argument_tree_spec.rb +159 -0
  67. data/spec/unit/processor/ffmpeg_spec.rb +467 -0
  68. data/spec/unit/processor/image_magick_spec.rb +260 -0
  69. data/spec/unit/processor/one_shot_spec.rb +70 -0
  70. data/spec/unit/reflection_spec.rb +338 -0
  71. data/spec/unit/stream_spec.rb +234 -0
  72. data/spec/unit/style_set_spec.rb +44 -0
  73. data/spec/unit/style_spec.rb +51 -0
  74. data/spec/unit/validations_spec.rb +491 -0
  75. data/spec/unit/vector2_spec.rb +27 -0
  76. data/tasks/bulldog_tasks.rake +4 -0
  77. 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