forked from woahdae/attachment_fu
-
Notifications
You must be signed in to change notification settings - Fork 0
/
s3_backend.rb
342 lines (310 loc) · 13.6 KB
/
s3_backend.rb
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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
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>
#
# === Using different bucket names on different models
#
# By default the bucket name that the file will be stored to is the one specified by the
# <tt>:bucket_name</tt> key in the amazon_s3.yml file. You can use the <tt>:bucket_key</tt> option
# to overide this behavior on a per model basis. For instance if you want a bucket that will hold
# only Photos you can do this:
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :s3, :bucket_key => :photo_bucket_name
# end
#
# And then your amazon_s3.yml file needs to look like this.
#
# 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
# photo_bucket_name: appname_photos
# access_key_id: <your key>
# secret_access_key: <your key>
#
# If the bucket_key you specify is not there in a certain environment then attachment_fu will
# default to the <tt>bucket_name</tt> key. This way you only have to create special buckets
# this can be helpful if you only need special buckets in certain environments.
#
# === 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(ERB.new(File.read(@@s3_config_path)).result)[RAILS_ENV].symbolize_keys
#rescue
# raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
end
bucket_key = base.attachment_options[:bucket_key]
if bucket_key and s3_config[bucket_key.to_sym]
@@bucket_name = s3_config[bucket_key.to_sym]
else
@@bucket_name = s3_config[:bucket_name]
end
Base.establish_connection!(s3_config.slice(:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy))
# 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)
options = args.extract_options!
thumbnail = args.shift
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