b81fbde8a4152e195a2b42365087b9c62b04fad5
[selectricity] / 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       # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
21       # 
22       #   development:
23       #     bucket_name: appname_development
24       #     access_key_id: <your key>
25       #     secret_access_key: <your key>
26       #   
27       #   test:
28       #     bucket_name: appname_test
29       #     access_key_id: <your key>
30       #     secret_access_key: <your key>
31       #   
32       #   production:
33       #     bucket_name: appname
34       #     access_key_id: <your key>
35       #     secret_access_key: <your key>
36       #
37       # You can change the location of the config path by passing a full path to the :s3_config_path option.
38       #
39       #   has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
40       #
41       # === Required configuration parameters
42       #
43       # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
44       # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
45       # * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
46       #
47       # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
48       #
49       # == About bucket names
50       #
51       # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
52       # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
53       # implementation to the development, test, and production environments.
54       #
55       # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
56       #
57       # === Optional configuration parameters
58       #
59       # * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
60       # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
61       # * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
62       #
63       # == Usage
64       #
65       # To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
66       #
67       #   class Photo < ActiveRecord::Base
68       #     has_attachment :storage => :s3
69       #   end
70       #
71       # === Customizing the path
72       #
73       # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
74       # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
75       # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
76       # option:
77       #
78       #   class Photo < ActiveRecord::Base
79       #     has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
80       #   end
81       #
82       # Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
83       #
84       # === Permissions
85       #
86       # By default, files are stored on S3 with public access permissions. You can customize this using
87       # the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are 
88       # <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
89       #
90       # === Other options
91       #
92       # Of course, all the usual configuration options apply, such as content_type and thumbnails:
93       #
94       #   class Photo < ActiveRecord::Base
95       #     has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
96       #     has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
97       #   end
98       #
99       # === Accessing S3 URLs
100       #
101       # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
102       # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
103       #
104       #   @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
105       #
106       # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
107       # The optional thumbnail argument will output the thumbnail's filename (if any).
108       #
109       # Additionally, you can get an object's base path relative to the bucket root using
110       # <tt>base_path</tt>:
111       #
112       #   @photo.file_base_path # => photos/1
113       #
114       # And the full path (including the filename) using <tt>full_filename</tt>:
115       #
116       #   @photo.full_filename # => photos/
117       #
118       # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
119       # You can retrieve the bucket name using the <tt>bucket_name</tt> method.
120       module S3Backend
121         class RequiredLibraryNotFoundError < StandardError; end
122         class ConfigFileNotFoundError < StandardError; end
123
124         def self.included(base) #:nodoc:
125           mattr_reader :bucket_name, :s3_config
126           
127           begin
128             require 'aws/s3'
129             include AWS::S3
130           rescue LoadError
131             raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
132           end
133
134           begin
135             @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
136             @@s3_config = @@s3_config = YAML.load_file(@@s3_config_path)[RAILS_ENV].symbolize_keys
137           #rescue
138           #  raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
139           end
140
141           @@bucket_name = s3_config[:bucket_name]
142
143           Base.establish_connection!(
144             :access_key_id     => s3_config[:access_key_id],
145             :secret_access_key => s3_config[:secret_access_key],
146             :server            => s3_config[:server],
147             :port              => s3_config[:port],
148             :use_ssl           => s3_config[:use_ssl]
149           )
150
151           # Bucket.create(@@bucket_name)
152
153           base.before_update :rename_file
154         end
155
156         def self.protocol
157           @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
158         end
159         
160         def self.hostname
161           @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
162         end
163         
164         def self.port_string
165           @port_string ||= s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80) ? '' : ":#{s3_config[:port]}"
166         end
167
168         module ClassMethods
169           def s3_protocol
170             Technoweenie::AttachmentFu::Backends::S3Backend.protocol
171           end
172           
173           def s3_hostname
174             Technoweenie::AttachmentFu::Backends::S3Backend.hostname
175           end
176           
177           def s3_port_string
178             Technoweenie::AttachmentFu::Backends::S3Backend.port_string
179           end
180         end
181
182         # Overwrites the base filename writer in order to store the old filename
183         def filename=(value)
184           @old_filename = filename unless filename.nil? || @old_filename
185           write_attribute :filename, sanitize_filename(value)
186         end
187
188         # The attachment ID used in the full path of a file
189         def attachment_path_id
190           ((respond_to?(:parent_id) && parent_id) || id).to_s
191         end
192
193         # The pseudo hierarchy containing the file relative to the bucket name
194         # Example: <tt>:table_name/:id</tt>
195         def base_path
196           File.join(attachment_options[:path_prefix], attachment_path_id)
197         end
198
199         # The full path to the file relative to the bucket name
200         # Example: <tt>:table_name/:id/:filename</tt>
201         def full_filename(thumbnail = nil)
202           File.join(base_path, thumbnail_name_for(thumbnail))
203         end
204
205         # All public objects are accessible via a GET request to the S3 servers. You can generate a 
206         # url for an object using the s3_url method.
207         #
208         #   @photo.s3_url
209         #
210         # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
211         # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
212         # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
213         #
214         # The optional thumbnail argument will output the thumbnail's filename (if any).
215         def s3_url(thumbnail = nil)
216           File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
217         end
218         alias :public_filename :s3_url
219
220         # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an 
221         # authenticated url for an object like this:
222         #
223         #   @photo.authenticated_s3_url
224         #
225         # By default authenticated urls expire 5 minutes after they were generated.
226         #
227         # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
228         # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
229         #
230         #   # Absolute expiration date (October 13th, 2025)
231         #   @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
232         #   
233         #   # Expiration in five hours from now
234         #   @photo.authenticated_s3_url(:expires_in => 5.hours)
235         #
236         # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
237         # By default, the ssl settings for the current connection will be used:
238         #
239         #   @photo.authenticated_s3_url(:use_ssl => true)
240         #
241         # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
242         #
243         #   @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
244         def authenticated_s3_url(*args)
245           thumbnail = args.first.is_a?(String) ? args.first : nil
246           options   = args.last.is_a?(Hash)    ? args.last  : {}
247           S3Object.url_for(full_filename(thumbnail), bucket_name, options)
248         end
249
250         def create_temp_file
251           write_to_temp_file current_data
252         end
253
254         def current_data
255           S3Object.value full_filename, bucket_name
256         end
257
258         def s3_protocol
259           Technoweenie::AttachmentFu::Backends::S3Backend.protocol
260         end
261         
262         def s3_hostname
263           Technoweenie::AttachmentFu::Backends::S3Backend.hostname
264         end
265           
266         def s3_port_string
267           Technoweenie::AttachmentFu::Backends::S3Backend.port_string
268         end
269
270         protected
271           # Called in the after_destroy callback
272           def destroy_file
273             S3Object.delete full_filename, bucket_name
274           end
275
276           def rename_file
277             return unless @old_filename && @old_filename != filename
278             
279             old_full_filename = File.join(base_path, @old_filename)
280
281             S3Object.rename(
282               old_full_filename,
283               full_filename,
284               bucket_name,
285               :access => attachment_options[:s3_access]
286             )
287
288             @old_filename = nil
289             true
290           end
291
292           def save_to_storage
293             if save_attachment?
294               S3Object.store(
295                 full_filename,
296                 (temp_path ? File.open(temp_path) : temp_data),
297                 bucket_name,
298                 :content_type => content_type,
299                 :access => attachment_options[:s3_access]
300               )
301             end
302
303             @old_filename = nil
304             true
305           end
306       end
307     end
308   end
309 end

Benjamin Mako Hill || Want to submit a patch?