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,331 @@
1
+ module Paperclip
2
+ module Interpolations
3
+ # Returns the youtube_id of the instance.
4
+ def youtube_id attachment, style
5
+ attachment.instance.youtube_id
6
+ end
7
+ end
8
+
9
+ module Storage
10
+ # Youtube video hosting, easy place to share videos for
11
+ # You can find at http://www.youtube.com/
12
+ # There are a few S3-specific options for has_attached_file:
13
+ # * +youtube_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
14
+ # to a YAML file containing the +login_name+ and +login_password+ and +youttube_username+
15
+ # and +developer_key+ that you can got it from your account on Youtube.
16
+ # You can 'environment-space' this just like you do to your
17
+ # database.yml file, so different environments can use different accounts:
18
+ # development:
19
+ # login_name: dr-click...
20
+ # login_password: 123...
21
+ # production:
22
+ # login_name: dr-click...
23
+ # login_password: 123...
24
+ #
25
+
26
+ module Youtube
27
+ def self.extended base
28
+ begin
29
+ require 'net/http'
30
+ rescue LoadError => e
31
+ log("(Error) #{e.message}")
32
+ e.message << " (Can't find required library 'net/http')"
33
+ raise e
34
+ end
35
+ begin
36
+ require 'net/https'
37
+ rescue LoadError => e
38
+ log("(Error) #{e.message}")
39
+ e.message << " (Can't find required library 'net/https')"
40
+ raise e
41
+ end
42
+ begin
43
+ require 'mime/types'
44
+ rescue LoadError => e
45
+ log("(Error) #{e.message}")
46
+ e.message << " (You may need to install the mime-types gem)"
47
+ raise e
48
+ end
49
+ begin
50
+ require 'builder'
51
+ rescue LoadError => e
52
+ log("(Error) #{e.message}")
53
+ e.message << " (You may need to install the builder gem)"
54
+ raise e
55
+ end
56
+ begin
57
+ require 'rexml/document'
58
+ rescue LoadError => e
59
+ log("(Error) #{e.message}")
60
+ e.message << " (Can't find required library 'rexml/document')"
61
+ raise e
62
+ end
63
+
64
+ base.instance_eval do
65
+ @youtube_options = @options[:youtube_options] || {}
66
+
67
+ @developer_key = @options[:developer_key] || @youtube_options[:developer_key]
68
+ @login_name = @options[:login_name] || @youtube_options[:login_name]
69
+ @login_password = @options[:login_password] || @youtube_options[:login_password]
70
+ @youttube_username = @options[:youttube_username] || @youtube_options[:youttube_username]
71
+
72
+ @auth_host = @options[:auth_host] || @youtube_options[:auth_host] || 'www.google.com'
73
+ @auth_path = @options[:auth_path] || @youtube_options[:auth_path] || '/youtube/accounts/ClientLogin'
74
+
75
+ @upload_host = @options[:upload_host] || @youtube_options[:upload_host] || 'uploads.gdata.youtube.com'
76
+ @upload_path = @options[:upload_path] || @youtube_options[:upload_path] || "/feeds/api/users/#{@youttube_username}/uploads"
77
+
78
+ @data_host = @options[:data_host] || @youtube_options[:data_host] || 'gdata.youtube.com'
79
+ end
80
+
81
+ Paperclip.interpolates(:youtube_url) do |attachment, style|
82
+ style = :thumbnail_1 if style == :thumbnail
83
+ if style == :original
84
+ 'http://www.youtube.com/watch?v=:youtube_id'
85
+ else
86
+ "http://i.ytimg.com/vi/:youtube_id/#{style.to_s.split('_').last.to_i}.jpg"
87
+ end
88
+ end
89
+ end
90
+
91
+ def login_name
92
+ @login_name
93
+ end
94
+ def login_password
95
+ @login_password
96
+ end
97
+ def developer_key
98
+ @developer_key
99
+ end
100
+ def youttube_username
101
+ @youttube_username
102
+ end
103
+ def auth_host
104
+ @auth_host
105
+ end
106
+ def auth_path
107
+ @auth_path
108
+ end
109
+ def upload_host
110
+ @upload_host
111
+ end
112
+ def upload_path
113
+ @upload_path
114
+ end
115
+ def data_host
116
+ @data_host
117
+ end
118
+ def token
119
+ @token ||= begin
120
+ http = Net::HTTP.new("www.google.com", 443)
121
+ http.use_ssl = true
122
+ body = "Email=#{YoutubeChain.esc login_name}&Passwd=#{YoutubeChain.esc login_password}&service=youtube&source=#{YoutubeChain.esc youttube_username}"
123
+ response = http.post("/youtube/accounts/ClientLogin", body, "Content-Type" => "application/x-www-form-urlencoded")
124
+ raise response.body[/Error=(.+)/,1] if response.code.to_i != 200
125
+ @token = response.body[/Auth=(.+)/, 1]
126
+ end
127
+ end
128
+
129
+ def update_youtube_id(data)
130
+ doc = REXML::Document.new data
131
+ id = doc.root.elements["id"].text.split("/").last if doc && doc.root
132
+
133
+ if id
134
+ video = Video.find self.instance.id
135
+ video.update_attribute(:youtube_id, id)
136
+ else
137
+ log("(Error) Video has no id, please reupload to Youtube.")
138
+ raise "Video has no id, please reupload to Youtube."
139
+ end
140
+ end
141
+
142
+ def youtube_delete
143
+ http = Net::HTTP.new(data_host)
144
+ headers = {
145
+ 'Content-Type' => 'application/atom+xml',
146
+ 'Authorization' => "GoogleLogin auth=#{token}",
147
+ 'GData-Version' => '2',
148
+ 'X-GData-Key' => "key=#{developer_key}"
149
+ }
150
+
151
+ resp = http.delete(upload_path+"/#{self.instance.youtube_id}", headers)
152
+ if resp.code != "200"
153
+ log("(Error) Couldn't delete the video from Youtube")
154
+ raise "Couldn't delete the video from Youtube"
155
+ end
156
+ end
157
+
158
+ def exists?(style_name = default_style)
159
+ http = Net::HTTP.new(data_host)
160
+ headers = {
161
+ 'Content-Type' => 'application/atom+xml',
162
+ 'Authorization' => "GoogleLogin auth=#{token}",
163
+ 'GData-Version' => '2',
164
+ 'X-GData-Key' => "key=#{developer_key}"
165
+ }
166
+
167
+ resp= http.get(upload_path+"/#{self.instance.youtube_id}", headers)
168
+ if resp.code == "200"
169
+ return true
170
+ elsif
171
+ log("(Error) Couldn't find the video '#{self.instance.youtube_id}'")
172
+ return false
173
+ end
174
+ end
175
+
176
+ # Returns representation of the data of the file assigned to the given
177
+ # style, in the format most representative of the current storage.
178
+ def to_file style_name = default_style
179
+ end
180
+
181
+ def boundary
182
+ "An43094fu"
183
+ end
184
+
185
+ def request_xml(opts)
186
+ b = Builder::XmlMarkup.new
187
+ b.instruct!
188
+ b.entry(:xmlns => "http://www.w3.org/2005/Atom", 'xmlns:media' => "http://search.yahoo.com/mrss/", 'xmlns:yt' => "http://gdata.youtube.com/schemas/2007") do | m |
189
+ m.tag!("media:group") do | mg |
190
+ mg.tag!("media:title", opts[:title], :type => "plain")
191
+ mg.tag!("media:description", opts[:description], :type => "plain")
192
+ mg.tag!("media:keywords", opts[:keywords].join(","))
193
+ mg.tag!('media:category', opts[:category], :scheme => "http://gdata.youtube.com/schemas/2007/categories.cat")
194
+ mg.tag!('yt:private') if opts[:private]
195
+ end
196
+ end.to_s
197
+ end
198
+
199
+ def request_io(data, opts)
200
+ post_body = [
201
+ "--#{boundary}\r\n",
202
+ "Content-Type: application/atom+xml; charset=UTF-8\r\n\r\n",
203
+ request_xml(opts),
204
+ "\r\n--#{boundary}\r\n",
205
+ "Content-Type: #{opts[:mime_type]}\r\nContent-Transfer-Encoding: binary\r\n\r\n",
206
+ data,
207
+ "\r\n--#{boundary}--\r\n",
208
+ ]
209
+
210
+ YoutubeChain.new(post_body)
211
+ end
212
+
213
+ def authorization_headers
214
+ {
215
+ "Authorization" => "GoogleLogin auth=#{token}",
216
+ "X-GData-Client" => "#{youttube_username}",
217
+ "X-GData-Key" => "key=#{developer_key}"
218
+ }
219
+ end
220
+
221
+ def youtube_upload(data, video_file)
222
+ opts = { :mime_type => MIME::Types.type_for(video_file).join(),
223
+ :title => self.instance.respond_to?(:title) && !self.instance.title.blank? ? self.instance.title.blank? : video_file,
224
+ :description => self.instance.respond_to?(:description) && !self.instance.description.blank? ? self.instance.description.blank? : video_file,
225
+ :category => 'People',
226
+ :keywords => [],
227
+ :filename => video_file}
228
+
229
+ post_body_io = request_io(data, opts)
230
+ upload_headers = authorization_headers.merge({
231
+ "Slug" => "#{opts[:filename]}",
232
+ "Content-Type" => "multipart/related; boundary=#{boundary}",
233
+ "Content-Length" => "#{post_body_io.expected_length}"
234
+ })
235
+
236
+ path = upload_path
237
+
238
+ Net::HTTP.start(upload_host) do | session |
239
+ post = Net::HTTP::Post.new(path, upload_headers)
240
+ post.body_stream = post_body_io
241
+ response = session.request(post)
242
+
243
+ if response.code == "201"
244
+ update_youtube_id response.body
245
+ else
246
+ log("(Error) Couldn't upload the video to Youtube >> #{response.body}")
247
+ raise "Couldn't upload the video to Youtube >> #{response.body}"
248
+ end
249
+ end
250
+ end
251
+
252
+ def flush_writes #:nodoc:
253
+ @queued_for_write.each do |style, file|
254
+ begin
255
+ log("Uploading to youtube : #{self.instance.media_file_name}")
256
+ youtube_upload(File.open(file.path), self.instance.media_file_name)
257
+ rescue => e
258
+ log("(Error) #{e.message} - #{e.backtrace.inspect}")
259
+ raise e.message
260
+ end
261
+ end
262
+ @queued_for_write = {}
263
+ end
264
+
265
+ def flush_deletes #:nodoc:
266
+ @queued_for_delete.each do |path|
267
+ begin
268
+ log("deleting from youtube : #{self.instance.youtube_id}")
269
+ youtube_delete
270
+ rescue => e
271
+ log("(Error) #{e.message}")
272
+ raise e.message
273
+ end
274
+ end
275
+ @queued_for_delete = []
276
+ end
277
+ end
278
+
279
+
280
+ end
281
+ end
282
+
283
+
284
+ class YoutubeChain
285
+ attr_accessor :autoclose
286
+ def self.esc(s)
287
+ s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ', '+')
288
+ end
289
+
290
+ def initialize(*any_ios)
291
+ @autoclose = true
292
+ @chain = any_ios.flatten.map{|e| e.respond_to?(:read) ? e : StringIO.new(e.to_s) }
293
+ end
294
+
295
+ def read(buffer_size = 1024)
296
+ current_io = @chain.shift
297
+ return false if !current_io
298
+
299
+ buf = current_io.read(buffer_size)
300
+ if !buf && @chain.empty? # End of streams
301
+ release_handle(current_io) if @autoclose
302
+ false
303
+ elsif !buf # This IO is depleted, but next one is available
304
+ release_handle(current_io) if @autoclose
305
+ read(buffer_size)
306
+ elsif buf.length < buffer_size # This IO is depleted, but we were asked for more
307
+ release_handle(current_io) if @autoclose
308
+ buf + (read(buffer_size - buf.length) || '') # and recurse
309
+ else # just return the buffer
310
+ @chain.unshift(current_io) # put the current back
311
+ buf
312
+ end
313
+ end
314
+
315
+ def expected_length
316
+ @chain.inject(0) do | len, io |
317
+ if io.respond_to?(:length)
318
+ len + (io.length - io.pos)
319
+ elsif io.is_a?(File)
320
+ len + File.size(io.path) - io.pos
321
+ else
322
+ raise "Cannot predict length of #{io.inspect}"
323
+ end
324
+ end
325
+ end
326
+
327
+ private
328
+ def release_handle(io)
329
+ io.close if io.respond_to?(:close)
330
+ end
331
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ module Paperclip
3
+ # The Style class holds the definition of a thumbnail style, applying
4
+ # whatever processing is required to normalize the definition and delaying
5
+ # the evaluation of block parameters until useful context is available.
6
+
7
+ class Style
8
+
9
+ attr_reader :name, :attachment, :format
10
+
11
+ # Creates a Style object. +name+ is the name of the attachment,
12
+ # +definition+ is the style definition from has_attached_file, which
13
+ # can be string, array or hash
14
+ def initialize name, definition, attachment
15
+ @name = name
16
+ @attachment = attachment
17
+ if definition.is_a? Hash
18
+ @geometry = definition.delete(:geometry)
19
+ @format = definition.delete(:format)
20
+ @processors = definition.delete(:processors)
21
+ @other_args = definition
22
+ else
23
+ @geometry, @format = [definition, nil].flatten[0..1]
24
+ @other_args = {}
25
+ end
26
+ @format = nil if @format.blank?
27
+ end
28
+
29
+ # retrieves from the attachment the processors defined in the has_attached_file call
30
+ # (which method (in the attachment) will call any supplied procs)
31
+ # There is an important change of interface here: a style rule can set its own processors
32
+ # by default we behave as before, though.
33
+ def processors
34
+ @processors || attachment.processors
35
+ end
36
+
37
+ # retrieves from the attachment the whiny setting
38
+ def whiny
39
+ attachment.whiny
40
+ end
41
+
42
+ # returns true if we're inclined to grumble
43
+ def whiny?
44
+ !!whiny
45
+ end
46
+
47
+ def convert_options
48
+ attachment.send(:extra_options_for, name)
49
+ end
50
+
51
+ # returns the geometry string for this style
52
+ # if a proc has been supplied, we call it here
53
+ def geometry
54
+ @geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
55
+ end
56
+
57
+ # Supplies the hash of options that processors expect to receive as their second argument
58
+ # Arguments other than the standard geometry, format etc are just passed through from
59
+ # initialization and any procs are called here, just before post-processing.
60
+ def processor_options
61
+ args = {}
62
+ @other_args.each do |k,v|
63
+ args[k] = v.respond_to?(:call) ? v.call(attachment) : v
64
+ end
65
+ [:processors, :geometry, :format, :whiny, :convert_options].each do |k|
66
+ (arg = send(k)) && args[k] = arg
67
+ end
68
+ args
69
+ end
70
+
71
+ # Supports getting and setting style properties with hash notation to ensure backwards-compatibility
72
+ # eg. @attachment.styles[:large][:geometry]@ will still work
73
+ def [](key)
74
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
75
+ send(key)
76
+ elsif defined? @other_args[key]
77
+ @other_args[key]
78
+ end
79
+ end
80
+
81
+ def []=(key, value)
82
+ if [:name, :convert_options, :whiny, :processors, :geometry, :format].include?(key)
83
+ send("#{key}=".intern, value)
84
+ else
85
+ @other_args[key] = value
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,79 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options, :source_file_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, options = {}, attachment = nil
14
+ super
15
+
16
+ geometry = options[:geometry]
17
+ @file = file
18
+ @crop = geometry[-1,1] == '#'
19
+ @target_geometry = Geometry.parse geometry
20
+ @current_geometry = Geometry.from_file @file
21
+ @source_file_options = options[:source_file_options]
22
+ @convert_options = options[:convert_options]
23
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
24
+ @format = options[:format]
25
+
26
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
27
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
28
+
29
+ @current_format = File.extname(@file.path)
30
+ @basename = File.basename(@file.path, @current_format)
31
+
32
+ end
33
+
34
+ # Returns true if the +target_geometry+ is meant to crop.
35
+ def crop?
36
+ @crop
37
+ end
38
+
39
+ # Returns true if the image is meant to make use of additional convert options.
40
+ def convert_options?
41
+ !@convert_options.nil? && !@convert_options.empty?
42
+ end
43
+
44
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
45
+ # that contains the new image.
46
+ def make
47
+ src = @file
48
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
49
+ dst.binmode
50
+
51
+ begin
52
+ parameters = []
53
+ parameters << source_file_options
54
+ parameters << ":source"
55
+ parameters << transformation_command
56
+ parameters << convert_options
57
+ parameters << ":dest"
58
+
59
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
60
+
61
+ success = Paperclip.run("convert", parameters, :source => "#{File.expand_path(src.path)}[0]", :dest => File.expand_path(dst.path))
62
+ rescue PaperclipCommandLineError => e
63
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
64
+ end
65
+
66
+ dst
67
+ end
68
+
69
+ # Returns the command ImageMagick's +convert+ needs to transform the image
70
+ # into the thumbnail.
71
+ def transformation_command
72
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
73
+ trans = []
74
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
75
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
76
+ trans
77
+ end
78
+ end
79
+ end