paperclip-youtube 2.3.8.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 (62) hide show
  1. data/LICENSE +26 -0
  2. data/README.md +91 -0
  3. data/Rakefile +80 -0
  4. data/generators/paperclip/USAGE +5 -0
  5. data/generators/paperclip/paperclip_generator.rb +27 -0
  6. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  7. data/init.rb +1 -0
  8. data/lib/generators/paperclip/USAGE +8 -0
  9. data/lib/generators/paperclip/paperclip_generator.rb +31 -0
  10. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  11. data/lib/paperclip.rb +378 -0
  12. data/lib/paperclip/attachment.rb +376 -0
  13. data/lib/paperclip/callback_compatability.rb +61 -0
  14. data/lib/paperclip/command_line.rb +86 -0
  15. data/lib/paperclip/geometry.rb +115 -0
  16. data/lib/paperclip/interpolations.rb +130 -0
  17. data/lib/paperclip/iostream.rb +45 -0
  18. data/lib/paperclip/matchers.rb +33 -0
  19. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  20. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +75 -0
  21. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  22. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  23. data/lib/paperclip/processor.rb +58 -0
  24. data/lib/paperclip/railtie.rb +24 -0
  25. data/lib/paperclip/storage.rb +3 -0
  26. data/lib/paperclip/storage/filesystem.rb +73 -0
  27. data/lib/paperclip/storage/s3.rb +192 -0
  28. data/lib/paperclip/storage/youtube.rb +331 -0
  29. data/lib/paperclip/style.rb +90 -0
  30. data/lib/paperclip/thumbnail.rb +79 -0
  31. data/lib/paperclip/upfile.rb +55 -0
  32. data/lib/paperclip/version.rb +3 -0
  33. data/lib/tasks/paperclip.rake +72 -0
  34. data/rails/init.rb +2 -0
  35. data/shoulda_macros/paperclip.rb +118 -0
  36. data/test/attachment_test.rb +921 -0
  37. data/test/command_line_test.rb +138 -0
  38. data/test/database.yml +4 -0
  39. data/test/fixtures/12k.png +0 -0
  40. data/test/fixtures/50x50.png +0 -0
  41. data/test/fixtures/5k.png +0 -0
  42. data/test/fixtures/bad.png +1 -0
  43. data/test/fixtures/s3.yml +8 -0
  44. data/test/fixtures/text.txt +0 -0
  45. data/test/fixtures/twopage.pdf +0 -0
  46. data/test/fixtures/uppercase.PNG +0 -0
  47. data/test/geometry_test.rb +177 -0
  48. data/test/helper.rb +146 -0
  49. data/test/integration_test.rb +570 -0
  50. data/test/interpolations_test.rb +143 -0
  51. data/test/iostream_test.rb +71 -0
  52. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  53. data/test/matchers/validate_attachment_content_type_matcher_test.rb +47 -0
  54. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  55. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  56. data/test/paperclip_test.rb +301 -0
  57. data/test/processor_test.rb +10 -0
  58. data/test/storage_test.rb +386 -0
  59. data/test/style_test.rb +141 -0
  60. data/test/thumbnail_test.rb +227 -0
  61. data/test/upfile_test.rb +36 -0
  62. metadata +195 -0
@@ -0,0 +1,376 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Attachment class manages the files for a given attachment. It saves
4
+ # when the model saves, deletes when the model is destroyed, and processes
5
+ # the file upon assignment.
6
+ class Attachment
7
+ include IOStream
8
+
9
+ def self.default_options
10
+ @default_options ||= {
11
+ :url => "/system/:attachment/:id/:style/:filename",
12
+ :path => ":rails_root/public:url",
13
+ :styles => {},
14
+ :processors => [:thumbnail],
15
+ :convert_options => {},
16
+ :default_url => "/:attachment/:style/missing.png",
17
+ :default_style => :original,
18
+ :storage => :filesystem,
19
+ :use_timestamp => true,
20
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
21
+ :use_default_time_zone => true,
22
+ :hash_digest => "SHA1",
23
+ :hash_data => ":class/:attachment/:id/:style/:updated_at"
24
+ }
25
+ end
26
+
27
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options
28
+ attr_accessor :post_processing
29
+
30
+ # Creates an Attachment object. +name+ is the name of the attachment,
31
+ # +instance+ is the ActiveRecord object instance it's attached to, and
32
+ # +options+ is the same as the hash passed to +has_attached_file+.
33
+ def initialize name, instance, options = {}
34
+ @name = name
35
+ @instance = instance
36
+
37
+ options = self.class.default_options.merge(options)
38
+
39
+ @url = options[:url]
40
+ @url = @url.call(self) if @url.is_a?(Proc)
41
+ @path = options[:path]
42
+ @path = @path.call(self) if @path.is_a?(Proc)
43
+ @styles = options[:styles]
44
+ @normalized_styles = nil
45
+ @default_url = options[:default_url]
46
+ @default_style = options[:default_style]
47
+ @storage = options[:storage]
48
+ @use_timestamp = options[:use_timestamp]
49
+ @whiny = options[:whiny_thumbnails] || options[:whiny]
50
+ @use_default_time_zone = options[:use_default_time_zone]
51
+ @hash_digest = options[:hash_digest]
52
+ @hash_data = options[:hash_data]
53
+ @hash_secret = options[:hash_secret]
54
+ @convert_options = options[:convert_options]
55
+ @processors = options[:processors]
56
+ @options = options
57
+ @post_processing = true
58
+ @queued_for_delete = []
59
+ @queued_for_write = {}
60
+ @errors = {}
61
+ @dirty = false
62
+
63
+ initialize_storage
64
+ end
65
+
66
+ def styles
67
+ unless @normalized_styles
68
+ @normalized_styles = {}
69
+ (@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args|
70
+ @normalized_styles[name] = Paperclip::Style.new(name, args.dup, self)
71
+ end
72
+ end
73
+ @normalized_styles
74
+ end
75
+
76
+ def processors
77
+ @processors.respond_to?(:call) ? @processors.call(instance) : @processors
78
+ end
79
+
80
+ # What gets called when you call instance.attachment = File. It clears
81
+ # errors, assigns attributes, and processes the file. It
82
+ # also queues up the previous file for deletion, to be flushed away on
83
+ # #save of its host. In addition to form uploads, you can also assign
84
+ # another Paperclip attachment:
85
+ # new_user.avatar = old_user.avatar
86
+ def assign uploaded_file
87
+ ensure_required_accessors!
88
+
89
+ if uploaded_file.is_a?(Paperclip::Attachment)
90
+ uploaded_file = uploaded_file.to_file(:original)
91
+ close_uploaded_file = uploaded_file.respond_to?(:close)
92
+ end
93
+
94
+ return nil unless valid_assignment?(uploaded_file)
95
+
96
+ uploaded_file.binmode if uploaded_file.respond_to? :binmode
97
+ self.clear
98
+
99
+ return nil if uploaded_file.nil?
100
+
101
+ @queued_for_write[:original] = to_tempfile(uploaded_file)
102
+ instance_write(:file_name, uploaded_file.original_filename.strip)
103
+ instance_write(:content_type, uploaded_file.content_type.to_s.strip)
104
+ instance_write(:file_size, uploaded_file.size.to_i)
105
+ instance_write(:fingerprint, generate_fingerprint(uploaded_file))
106
+ instance_write(:updated_at, Time.now)
107
+
108
+ @dirty = true
109
+
110
+ post_process if @post_processing
111
+
112
+ # Reset the file size if the original file was reprocessed.
113
+ instance_write(:file_size, @queued_for_write[:original].size.to_i)
114
+ instance_write(:fingerprint, generate_fingerprint(@queued_for_write[:original]))
115
+ ensure
116
+ uploaded_file.close if close_uploaded_file
117
+ end
118
+
119
+ # Returns the public URL of the attachment, with a given style. Note that
120
+ # this does not necessarily need to point to a file that your web server
121
+ # can access and can point to an action in your app, if you need fine
122
+ # grained security. This is not recommended if you don't need the
123
+ # security, however, for performance reasons. Set use_timestamp to false
124
+ # if you want to stop the attachment update time appended to the url
125
+ def url(style_name = default_style, use_timestamp = @use_timestamp)
126
+ url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name)
127
+ use_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
128
+ end
129
+
130
+ # Returns the path of the attachment as defined by the :path option. If the
131
+ # file is stored in the filesystem the path refers to the path of the file
132
+ # on disk. If the file is stored in S3, the path is the "key" part of the
133
+ # URL, and the :bucket option refers to the S3 bucket.
134
+ def path style_name = default_style
135
+ original_filename.nil? ? nil : interpolate(@path, style_name)
136
+ end
137
+
138
+ # Alias to +url+
139
+ def to_s style_name = default_style
140
+ url(style_name)
141
+ end
142
+
143
+ # Returns an array containing the errors on this attachment.
144
+ def errors
145
+ @errors
146
+ end
147
+
148
+ # Returns true if there are changes that need to be saved.
149
+ def dirty?
150
+ @dirty
151
+ end
152
+
153
+ # Saves the file, if there are no errors. If there are, it flushes them to
154
+ # the instance's errors and returns false, cancelling the save.
155
+ def save
156
+ flush_deletes
157
+ flush_writes
158
+ @dirty = false
159
+ true
160
+ end
161
+
162
+ # Clears out the attachment. Has the same effect as previously assigning
163
+ # nil to the attachment. Does NOT save. If you wish to clear AND save,
164
+ # use #destroy.
165
+ def clear
166
+ queue_existing_for_delete
167
+ @errors = {}
168
+ end
169
+
170
+ # Destroys the attachment. Has the same effect as previously assigning
171
+ # nil to the attachment *and saving*. This is permanent. If you wish to
172
+ # wipe out the existing attachment but not save, use #clear.
173
+ def destroy
174
+ clear
175
+ save
176
+ end
177
+
178
+ # Returns the name of the file as originally assigned, and lives in the
179
+ # <attachment>_file_name attribute of the model.
180
+ def original_filename
181
+ instance_read(:file_name)
182
+ end
183
+
184
+ # Returns the size of the file as originally assigned, and lives in the
185
+ # <attachment>_file_size attribute of the model.
186
+ def size
187
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
188
+ end
189
+
190
+ # Returns the hash of the file as originally assigned, and lives in the
191
+ # <attachment>_fingerprint attribute of the model.
192
+ def fingerprint
193
+ instance_read(:fingerprint) || (@queued_for_write[:original] && generate_fingerprint(@queued_for_write[:original]))
194
+ end
195
+
196
+ # Returns the content_type of the file as originally assigned, and lives
197
+ # in the <attachment>_content_type attribute of the model.
198
+ def content_type
199
+ instance_read(:content_type)
200
+ end
201
+
202
+ # Returns the last modified time of the file as originally assigned, and
203
+ # lives in the <attachment>_updated_at attribute of the model.
204
+ def updated_at
205
+ time = instance_read(:updated_at)
206
+ time && time.to_f.to_i
207
+ end
208
+
209
+ # The time zone to use for timestamp interpolation. Using the default
210
+ # time zone ensures that results are consistent across all threads.
211
+ def time_zone
212
+ @use_default_time_zone ? Time.zone_default : Time.zone
213
+ end
214
+
215
+ # Returns a unique hash suitable for obfuscating the URL of an otherwise
216
+ # publicly viewable attachment.
217
+ def hash(style_name = default_style)
218
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @hash_secret
219
+ require 'openssl' unless defined?(OpenSSL)
220
+ data = interpolate(@hash_data, style_name)
221
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@hash_digest).new, @hash_secret, data)
222
+ end
223
+
224
+ def generate_fingerprint(source)
225
+ data = source.read
226
+ source.rewind if source.respond_to?(:rewind)
227
+ Digest::MD5.hexdigest(data)
228
+ end
229
+
230
+ # Paths and URLs can have a number of variables interpolated into them
231
+ # to vary the storage location based on name, id, style, class, etc.
232
+ # This method is a deprecated access into supplying and retrieving these
233
+ # interpolations. Future access should use either Paperclip.interpolates
234
+ # or extend the Paperclip::Interpolations module directly.
235
+ def self.interpolations
236
+ warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' +
237
+ 'and will be removed from future versions. ' +
238
+ 'Use Paperclip.interpolates instead')
239
+ Paperclip::Interpolations
240
+ end
241
+
242
+ # This method really shouldn't be called that often. It's expected use is
243
+ # in the paperclip:refresh rake task and that's it. It will regenerate all
244
+ # thumbnails forcefully, by reobtaining the original file and going through
245
+ # the post-process again.
246
+ def reprocess!(*style_args)
247
+ new_original = Tempfile.new("paperclip-reprocess")
248
+ new_original.binmode
249
+ if old_original = to_file(:original)
250
+ new_original.write( old_original.respond_to?(:get) ? old_original.get : old_original.read )
251
+ new_original.rewind
252
+
253
+ @queued_for_write = { :original => new_original }
254
+ post_process(*style_args)
255
+
256
+ old_original.close if old_original.respond_to?(:close)
257
+
258
+ save
259
+ else
260
+ true
261
+ end
262
+ rescue Errno::EACCES => e
263
+ warn "#{e} - skipping file"
264
+ false
265
+ end
266
+
267
+ # Returns true if a file has been assigned.
268
+ def file?
269
+ !original_filename.blank?
270
+ end
271
+
272
+ # Writes the attachment-specific attribute on the instance. For example,
273
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
274
+ # "avatar_file_name" field (assuming the attachment is called avatar).
275
+ def instance_write(attr, value)
276
+ setter = :"#{name}_#{attr}="
277
+ responds = instance.respond_to?(setter)
278
+ self.instance_variable_set("@_#{setter.to_s.chop}", value)
279
+ instance.send(setter, value) if responds || attr.to_s == "file_name"
280
+ end
281
+
282
+ # Reads the attachment-specific attribute on the instance. See instance_write
283
+ # for more details.
284
+ def instance_read(attr)
285
+ getter = :"#{name}_#{attr}"
286
+ responds = instance.respond_to?(getter)
287
+ cached = self.instance_variable_get("@_#{getter}")
288
+ return cached if cached
289
+ instance.send(getter) if responds || attr.to_s == "file_name"
290
+ end
291
+
292
+ private
293
+
294
+ def ensure_required_accessors! #:nodoc:
295
+ %w(file_name).each do |field|
296
+ unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
297
+ raise PaperclipError.new("#{@instance.class} model missing required attr_accessor for '#{name}_#{field}'")
298
+ end
299
+ end
300
+ end
301
+
302
+ def log message #:nodoc:
303
+ Paperclip.log(message)
304
+ end
305
+
306
+ def valid_assignment? file #:nodoc:
307
+ file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
308
+ end
309
+
310
+ def initialize_storage #:nodoc:
311
+ storage_class_name = @storage.to_s.capitalize
312
+ begin
313
+ @storage_module = Paperclip::Storage.const_get(storage_class_name)
314
+ rescue NameError
315
+ raise StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
316
+ end
317
+ self.extend(@storage_module)
318
+ end
319
+
320
+ def extra_options_for(style) #:nodoc:
321
+ all_options = convert_options[:all]
322
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
323
+ style_options = convert_options[style]
324
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
325
+
326
+ [ style_options, all_options ].compact.join(" ")
327
+ end
328
+
329
+ def post_process(*style_args) #:nodoc:
330
+ return if @queued_for_write[:original].nil?
331
+ instance.run_paperclip_callbacks(:post_process) do
332
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
333
+ post_process_styles(*style_args)
334
+ end
335
+ end
336
+ end
337
+
338
+ def post_process_styles(*style_args) #:nodoc:
339
+ styles.each do |name, style|
340
+ begin
341
+ if style_args.empty? || style_args.include?(name)
342
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
343
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
344
+ Paperclip.processor(processor).make(file, style.processor_options, self)
345
+ end
346
+ end
347
+ rescue PaperclipError => e
348
+ log("An error was received while processing: #{e.inspect}")
349
+ (@errors[:processing] ||= []) << e.message if @whiny
350
+ end
351
+ end
352
+ end
353
+
354
+ def interpolate pattern, style_name = default_style #:nodoc:
355
+ Paperclip::Interpolations.interpolate(pattern, self, style_name)
356
+ end
357
+
358
+ def queue_existing_for_delete #:nodoc:
359
+ return unless file?
360
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
361
+ path(style) if exists?(style)
362
+ end.compact
363
+ instance_write(:file_name, nil)
364
+ instance_write(:content_type, nil)
365
+ instance_write(:file_size, nil)
366
+ instance_write(:updated_at, nil)
367
+ end
368
+
369
+ def flush_errors #:nodoc:
370
+ @errors.each do |error, message|
371
+ [message].flatten.each {|m| instance.errors.add(name, m) }
372
+ end
373
+ end
374
+
375
+ end
376
+ end
@@ -0,0 +1,61 @@
1
+ module Paperclip
2
+ module CallbackCompatability
3
+ module Rails21
4
+ def self.included(base)
5
+ base.extend(Defining)
6
+ base.send(:include, Running)
7
+ end
8
+
9
+ module Defining
10
+ def define_paperclip_callbacks(*args)
11
+ args.each do |callback|
12
+ define_callbacks("before_#{callback}")
13
+ define_callbacks("after_#{callback}")
14
+ end
15
+ end
16
+ end
17
+
18
+ module Running
19
+ def run_paperclip_callbacks(callback, opts = nil, &blk)
20
+ # The overall structure of this isn't ideal since after callbacks run even if
21
+ # befores return false. But this is how rails 3's callbacks work, unfortunately.
22
+ if run_callbacks(:"before_#{callback}"){ |result, object| result == false } != false
23
+ blk.call
24
+ end
25
+ run_callbacks(:"after_#{callback}"){ |result, object| result == false }
26
+ end
27
+ end
28
+ end
29
+
30
+ module Rails3
31
+ def self.included(base)
32
+ base.extend(Defining)
33
+ base.send(:include, Running)
34
+ end
35
+
36
+ module Defining
37
+ def define_paperclip_callbacks(*callbacks)
38
+ define_callbacks *[callbacks, {:terminator => "result == false"}].flatten
39
+ callbacks.each do |callback|
40
+ eval <<-end_callbacks
41
+ def before_#{callback}(*args, &blk)
42
+ set_callback(:#{callback}, :before, *args, &blk)
43
+ end
44
+ def after_#{callback}(*args, &blk)
45
+ set_callback(:#{callback}, :after, *args, &blk)
46
+ end
47
+ end_callbacks
48
+ end
49
+ end
50
+ end
51
+
52
+ module Running
53
+ def run_paperclip_callbacks(callback, opts = nil, &block)
54
+ run_callbacks(callback, opts, &block)
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,86 @@
1
+ module Paperclip
2
+ class CommandLine
3
+ class << self
4
+ attr_accessor :path
5
+ end
6
+
7
+ def initialize(binary, params = "", options = {})
8
+ @binary = binary.dup
9
+ @params = params.dup
10
+ @options = options.dup
11
+ @swallow_stderr = @options.has_key?(:swallow_stderr) ? @options.delete(:swallow_stderr) : Paperclip.options[:swallow_stderr]
12
+ @expected_outcodes = @options.delete(:expected_outcodes)
13
+ @expected_outcodes ||= [0]
14
+ end
15
+
16
+ def command
17
+ cmd = []
18
+ cmd << full_path(@binary)
19
+ cmd << interpolate(@params, @options)
20
+ cmd << bit_bucket if @swallow_stderr
21
+ cmd.join(" ")
22
+ end
23
+
24
+ def run
25
+ Paperclip.log(command)
26
+ begin
27
+ output = self.class.send(:'`', command)
28
+ rescue Errno::ENOENT
29
+ raise Paperclip::CommandNotFoundError
30
+ end
31
+ if $?.exitstatus == 127
32
+ raise Paperclip::CommandNotFoundError
33
+ end
34
+ unless @expected_outcodes.include?($?.exitstatus)
35
+ raise Paperclip::PaperclipCommandLineError, "Command '#{command}' returned #{$?.exitstatus}. Expected #{@expected_outcodes.join(", ")}"
36
+ end
37
+ output
38
+ end
39
+
40
+ private
41
+
42
+ def full_path(binary)
43
+ [self.class.path, binary].compact.join("/")
44
+ end
45
+
46
+ def interpolate(pattern, vars)
47
+ # interpolates :variables and :{variables}
48
+ pattern.gsub(%r#:(?:\w+|\{\w+\})#) do |match|
49
+ key = match[1..-1]
50
+ key = key[1..-2] if key[0,1] == '{'
51
+ if invalid_variables.include?(key)
52
+ raise PaperclipCommandLineError,
53
+ "Interpolation of #{key} isn't allowed."
54
+ end
55
+ interpolation(vars, key) || match
56
+ end
57
+ end
58
+
59
+ def invalid_variables
60
+ %w(expected_outcodes swallow_stderr)
61
+ end
62
+
63
+ def interpolation(vars, key)
64
+ if vars.key?(key.to_sym)
65
+ shell_quote(vars[key.to_sym])
66
+ end
67
+ end
68
+
69
+ def shell_quote(string)
70
+ return "" if string.nil? or string.blank?
71
+ if self.class.unix?
72
+ string.split("'").map{|m| "'#{m}'" }.join("\\'")
73
+ else
74
+ %{"#{string}"}
75
+ end
76
+ end
77
+
78
+ def bit_bucket
79
+ self.class.unix? ? "2>/dev/null" : "2>NUL"
80
+ end
81
+
82
+ def self.unix?
83
+ File.exist?("/dev/null")
84
+ end
85
+ end
86
+ end