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