updated top the the new version of attachment_fu plugin to work out some
[selectricity-live] / vendor / plugins / attachment_fu / lib / technoweenie / attachment_fu / backends / s3_backend.rb
1 module Technoweenie # :nodoc:
2   module AttachmentFu # :nodoc:
3     module Backends
4       # = AWS::S3 Storage Backend
5       #
6       # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism
7       #
8       # == Requirements
9       #
10       # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either
11       # as a gem or a as a Rails plugin.
12       #
13       # == Configuration
14       #
15       # Configuration is done via <tt>RAILS_ROOT/config/amazon_s3.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
16       # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key.
17       # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon.
18       # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.
19       #
20       # If you wish to use Amazon CloudFront to serve the files, you can also specify a distibution domain for the bucket.
21       # To read more about CloudFront, visit http://aws.amazon.com/cloudfront
22       #
23       # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
24       #
25       #   development:
26       #     bucket_name: appname_development
27       #     access_key_id: <your key>
28       #     secret_access_key: <your key>
29       #     distribution_domain: XXXX.cloudfront.net
30       #
31       #   test:
32       #     bucket_name: appname_test
33       #     access_key_id: <your key>
34       #     secret_access_key: <your key>
35       #     distribution_domain: XXXX.cloudfront.net
36       #
37       #   production:
38       #     bucket_name: appname
39       #     access_key_id: <your key>
40       #     secret_access_key: <your key>
41       #     distribution_domain: XXXX.cloudfront.net
42       #
43       # You can change the location of the config path by passing a full path to the :s3_config_path option.
44       #
45       #   has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
46       #
47       # === Required configuration parameters
48       #
49       # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
50       # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
51       # * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
52       #
53       # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
54       #
55       # == About bucket names
56       #
57       # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
58       # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
59       # implementation to the development, test, and production environments.
60       #
61       # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
62       #
63       # === Optional configuration parameters
64       #
65       # * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
66       # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
67       # * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
68       # * <tt>:distribution_domain</tt> - The CloudFront distribution domain for the bucket.  This can either be the assigned
69       #     distribution domain (ie. XXX.cloudfront.net) or a chosen domain using a CNAME. See CloudFront for more details.
70       #
71       # == Usage
72       #
73       # To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
74       #
75       #   class Photo < ActiveRecord::Base
76       #     has_attachment :storage => :s3
77       #   end
78       #
79       # === Customizing the path
80       #
81       # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
82       # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
83       # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
84       # option:
85       #
86       #   class Photo < ActiveRecord::Base
87       #     has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
88       #   end
89       #
90       # Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
91       #
92       # === Using different bucket names on different models
93       #
94       # By default the bucket name that the file will be stored to is the one specified by the
95       # <tt>:bucket_name</tt> key in the amazon_s3.yml file.  You can use the <tt>:bucket_key</tt> option
96       # to overide this behavior on a per model basis.  For instance if you want a bucket that will hold
97       # only Photos you can do this:
98       #
99       #   class Photo < ActiveRecord::Base
100       #     has_attachment :storage => :s3, :bucket_key => :photo_bucket_name
101       #   end
102       #
103       # And then your amazon_s3.yml file needs to look like this.
104       #
105       #   development:
106       #     bucket_name: appname_development
107       #     access_key_id: <your key>
108       #     secret_access_key: <your key>
109       #
110       #   test:
111       #     bucket_name: appname_test
112       #     access_key_id: <your key>
113       #     secret_access_key: <your key>
114       #
115       #   production:
116       #     bucket_name: appname
117       #     photo_bucket_name: appname_photos
118       #     access_key_id: <your key>
119       #     secret_access_key: <your key>
120       #
121       #  If the bucket_key you specify is not there in a certain environment then attachment_fu will
122       #  default to the <tt>bucket_name</tt> key.  This way you only have to create special buckets
123       #  this can be helpful if you only need special buckets in certain environments.
124       #
125       # === Permissions
126       #
127       # By default, files are stored on S3 with public access permissions. You can customize this using
128       # the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are
129       # <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
130       #
131       # === Other options
132       #
133       # Of course, all the usual configuration options apply, such as content_type and thumbnails:
134       #
135       #   class Photo < ActiveRecord::Base
136       #     has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
137       #     has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
138       #   end
139       #
140       # === Accessing S3 URLs
141       #
142       # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
143       # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
144       #
145       #   @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
146       #
147       # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
148       # The optional thumbnail argument will output the thumbnail's filename (if any).
149       #
150       # Additionally, you can get an object's base path relative to the bucket root using
151       # <tt>base_path</tt>:
152       #
153       #   @photo.file_base_path # => photos/1
154       #
155       # And the full path (including the filename) using <tt>full_filename</tt>:
156       #
157       #   @photo.full_filename # => photos/
158       #
159       # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
160       # You can retrieve the bucket name using the <tt>bucket_name</tt> method.
161       # 
162       # === Accessing CloudFront URLs
163       # 
164       # You can get an object's CloudFront URL using the cloudfront_url accessor.  Using the example from above:
165       # @postcard.cloudfront_url # => http://XXXX.cloudfront.net/photos/1/mexico.jpg
166       #
167       # The resulting url is in the form: http://:distribution_domain/:table_name/:id/:file
168       #
169       # If you set :cloudfront to true in your model, the public_filename will be the CloudFront
170       # URL, not the S3 URL.
171       module S3Backend
172         class RequiredLibraryNotFoundError < StandardError; end
173         class ConfigFileNotFoundError < StandardError; end
174
175         def self.included(base) #:nodoc:
176           mattr_reader :bucket_name, :s3_config
177
178           begin
179             require 'aws/s3'
180             include AWS::S3
181           rescue LoadError
182             raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
183           end
184
185           begin
186             @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
187             @@s3_config = @@s3_config = YAML.load(ERB.new(File.read(@@s3_config_path)).result)[RAILS_ENV].symbolize_keys
188           #rescue
189           #  raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
190           end
191
192           bucket_key = base.attachment_options[:bucket_key]
193
194           if bucket_key and s3_config[bucket_key.to_sym]
195             eval_string = "def bucket_name()\n  \"#{s3_config[bucket_key.to_sym]}\"\nend"
196           else
197             eval_string = "def bucket_name()\n  \"#{s3_config[:bucket_name]}\"\nend"
198           end
199           base.class_eval(eval_string, __FILE__, __LINE__)
200
201           Base.establish_connection!(s3_config.slice(:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy))
202
203           # Bucket.create(@@bucket_name)
204
205           base.before_update :rename_file
206         end
207
208         def self.protocol
209           @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
210         end
211
212         def self.hostname
213           @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
214         end
215
216         def self.port_string
217           @port_string ||= (s3_config[:port].nil? || s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80)) ? '' : ":#{s3_config[:port]}"
218         end
219         
220         def self.distribution_domain
221           @distribution_domain = s3_config[:distribution_domain]
222         end
223
224         module ClassMethods
225           def s3_protocol
226             Technoweenie::AttachmentFu::Backends::S3Backend.protocol
227           end
228
229           def s3_hostname
230             Technoweenie::AttachmentFu::Backends::S3Backend.hostname
231           end
232
233           def s3_port_string
234             Technoweenie::AttachmentFu::Backends::S3Backend.port_string
235           end
236           
237           def cloudfront_distribution_domain
238             Technoweenie::AttachmentFu::Backends::S3Backend.distribution_domain
239           end
240         end
241
242         # Overwrites the base filename writer in order to store the old filename
243         def filename=(value)
244           @old_filename = filename unless filename.nil? || @old_filename
245           write_attribute :filename, sanitize_filename(value)
246         end
247
248         # The attachment ID used in the full path of a file
249         def attachment_path_id
250           ((respond_to?(:parent_id) && parent_id) || id).to_s
251         end
252
253         # The pseudo hierarchy containing the file relative to the bucket name
254         # Example: <tt>:table_name/:id</tt>
255         def base_path
256           File.join(attachment_options[:path_prefix], attachment_path_id)
257         end
258
259         # The full path to the file relative to the bucket name
260         # Example: <tt>:table_name/:id/:filename</tt>
261         def full_filename(thumbnail = nil)
262           File.join(base_path, thumbnail_name_for(thumbnail))
263         end
264
265         # All public objects are accessible via a GET request to the S3 servers. You can generate a
266         # url for an object using the s3_url method.
267         #
268         #   @photo.s3_url
269         #
270         # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
271         # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
272         # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
273         #
274         # The optional thumbnail argument will output the thumbnail's filename (if any).
275         def s3_url(thumbnail = nil)
276           File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
277         end
278         
279         # All public objects are accessible via a GET request to CloudFront. You can generate a
280         # url for an object using the cloudfront_url method.
281         #
282         #   @photo.cloudfront_url
283         #
284         # The resulting url is in the form: <tt>http://:distribution_domain/:table_name/:id/:file</tt> using
285         # the <tt>:distribution_domain</tt> variable set in the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
286         #
287         # The optional thumbnail argument will output the thumbnail's filename (if any).
288         def cloudfront_url(thumbnail = nil)
289           "http://" + cloudfront_distribution_domain + "/" + full_filename(thumbnail)
290         end
291         
292         def public_filename(*args)
293           if attachment_options[:cloudfront]
294             cloudfront_url(args)
295           else
296             s3_url(args)
297           end
298         end
299
300         # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
301         # authenticated url for an object like this:
302         #
303         #   @photo.authenticated_s3_url
304         #
305         # By default authenticated urls expire 5 minutes after they were generated.
306         #
307         # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
308         # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
309         #
310         #   # Absolute expiration date (October 13th, 2025)
311         #   @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
312         #
313         #   # Expiration in five hours from now
314         #   @photo.authenticated_s3_url(:expires_in => 5.hours)
315         #
316         # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
317         # By default, the ssl settings for the current connection will be used:
318         #
319         #   @photo.authenticated_s3_url(:use_ssl => true)
320         #
321         # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
322         #
323         #   @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
324         def authenticated_s3_url(*args)
325           options   = args.extract_options!
326           options[:expires_in] = options[:expires_in].to_i if options[:expires_in]
327           thumbnail = args.shift
328           S3Object.url_for(full_filename(thumbnail), bucket_name, options)
329         end
330
331         def create_temp_file
332           write_to_temp_file current_data
333         end
334
335         def current_data
336           S3Object.value full_filename, bucket_name
337         end
338
339         def s3_protocol
340           Technoweenie::AttachmentFu::Backends::S3Backend.protocol
341         end
342
343         def s3_hostname
344           Technoweenie::AttachmentFu::Backends::S3Backend.hostname
345         end
346
347         def s3_port_string
348           Technoweenie::AttachmentFu::Backends::S3Backend.port_string
349         end
350         
351         def cloudfront_distribution_domain
352           Technoweenie::AttachmentFu::Backends::S3Backend.distribution_domain
353         end
354
355         protected
356           # Called in the after_destroy callback
357           def destroy_file
358             S3Object.delete full_filename, bucket_name
359           end
360
361           def rename_file
362             return unless @old_filename && @old_filename != filename
363
364             old_full_filename = File.join(base_path, @old_filename)
365
366             S3Object.rename(
367               old_full_filename,
368               full_filename,
369               bucket_name,
370               :access => attachment_options[:s3_access]
371             )
372
373             @old_filename = nil
374             true
375           end
376
377           def save_to_storage
378             if save_attachment?
379               S3Object.store(
380                 full_filename,
381                 (temp_path ? File.open(temp_path) : temp_data),
382                 bucket_name,
383                 :content_type => content_type,
384                 :access => attachment_options[:s3_access]
385               )
386             end
387
388             @old_filename = nil
389             true
390           end
391       end
392     end
393   end
394 end

Benjamin Mako Hill || Want to submit a patch?