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