forked from thoughtbot/paperclip
/
storage.rb
432 lines (397 loc) · 18.4 KB
/
storage.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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
module Paperclip
module Storage
# The default place to store attachments is in the filesystem. Files on the local
# filesystem can be very easily served by Apache without requiring a hit to your app.
# They also can be processed more easily after they've been saved, as they're just
# normal files. There is one Filesystem-specific option for has_attached_file.
# * +path+: The location of the repository of attachments on disk. This can (and, in
# almost all cases, should) be coordinated with the value of the +url+ option to
# allow files to be saved into a place where Apache can serve them without
# hitting your app. Defaults to
# ":rails_root/public/:attachment/:id/:style/:basename.:extension"
# By default this places the files in the app's public directory which can be served
# directly. If you are using capistrano for deployment, a good idea would be to
# make a symlink to the capistrano-created system directory from inside your app's
# public directory.
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
module Filesystem
def self.extended base
end
def exists?(style = default_style)
if original_filename
File.exist?(path(style))
else
false
end
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
@queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
end
alias_method :to_io, :to_file
def flush_writes #:nodoc:
@queued_for_write.each do |style, file|
file.close
FileUtils.mkdir_p(File.dirname(path(style)))
logger.info("[paperclip] saving #{path(style)}")
FileUtils.mv(file.path, path(style))
FileUtils.chmod(0644, path(style))
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
logger.info("[paperclip] deleting #{path}")
FileUtils.rm(path) if File.exist?(path)
rescue Errno::ENOENT => e
# ignore file-not-found, let everything else pass
end
begin
while(true)
path = File.dirname(path)
FileUtils.rmdir(path)
end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
# Stop trying to remove parent directories
rescue SystemCallError => e
logger.info("[paperclip] There was an unexpected error while deleting directories: #{e.class}")
# Ignore it
end
end
@queued_for_delete = []
end
end
# Amazon's S3 file hosting service is a scalable, easy place to store files for
# distribution. You can find out more about it at http://aws.amazon.com/s3
# There are a few S3-specific options for has_attached_file:
# * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
# gives you. You can 'environment-space' this just like you do to your
# database.yml file, so different environments can use different accounts:
# development:
# access_key_id: 123...
# secret_access_key: 123...
# test:
# access_key_id: abc...
# secret_access_key: abc...
# production:
# access_key_id: 456...
# secret_access_key: 456...
# This is not required, however, and the file may simply look like this:
# access_key_id: 456...
# secret_access_key: 456...
# In which case, those access keys will be used in all environments. You can also
# put your bucket name in this file, instead of adding it to the code directly.
# This is useful when you want the same account but a different bucket for
# development versus production.
# * +s3_permissions+: This is a String that should be one of the "canned" access
# policies that S3 provides (more information can be found here:
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
# The default for Paperclip is "public-read".
# * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
# 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
# default), and 'https' when your :s3_permissions are anything else.
# * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
# * +bucket+: This is the name of the S3 bucket that will store your files. Remember
# that the bucket must be unique across all of Amazon S3. If the bucket does not exist
# Paperclip will attempt to create it. The bucket name will not be interpolated.
# You can define the bucket as a Proc if you want to determine it's name at runtime.
# Paperclip will call that Proc with attachment as the only argument.
# * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
# S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
# link in the +url+ entry for more information about S3 domains and buckets.
# * +url+: There are three options for the S3 url. You can choose to have the bucket's name
# placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
# Lastly, you can specify a CNAME (which requires the CNAME to be specified as
# :s3_alias_url. You can read more about CNAMEs and S3 at
# http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
# Normally, this won't matter in the slightest and you can leave the default (which is
# path-style, or :s3_path_url). But in some cases paths don't work and you need to use
# the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
# NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
# :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
# alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
# by S3.
# * +path+: This is the key under the bucket in which the file will be stored. The
# URL will be constructed from the bucket and the path. This is what you will want
# to interpolate. Keys should be unique, like filenames, and despite the fact that
# S3 (strictly speaking) does not support directories, you can still use a / to
# separate parts of your file name.
module S3
def self.extended base
require 'right_aws'
base.instance_eval do
@s3_credentials = parse_credentials(@options[:s3_credentials])
@bucket = @options[:bucket] || @s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
@s3_options = @options[:s3_options] || {}
@s3_permissions = @options[:s3_permissions] || 'public-read'
@s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
@s3_headers = @options[:s3_headers] || {}
@s3_host_alias = @options[:s3_host_alias]
@url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
end
base.class.interpolations[:s3_alias_url] = lambda do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
end
base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
"#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
end
base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
"#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
end
end
def s3
@s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
@s3_credentials[:secret_access_key],
@s3_options)
end
def s3_bucket
@s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
end
def bucket_name
@bucket
end
def s3_host_alias
@s3_host_alias
end
def parse_credentials creds
creds = find_credentials(creds).stringify_keys
(creds[ENV['RAILS_ENV']] || creds).symbolize_keys
end
def exists?(style = default_style)
s3_bucket.key(path(style)) ? true : false
end
def s3_protocol
@s3_protocol
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
@queued_for_write[style] || s3_bucket.key(path(style))
end
alias_method :to_io, :to_file
def flush_writes #:nodoc:
@queued_for_write.each do |style, file|
begin
logger.info("[paperclip] saving #{path(style)}")
key = s3_bucket.key(path(style))
key.data = file
key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
rescue RightAws::AwsError => e
raise
end
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
logger.info("[paperclip] deleting #{path}")
if file = s3_bucket.key(path)
file.delete
end
rescue RightAws::AwsError
# Ignore this.
end
end
@queued_for_delete = []
end
def find_credentials creds
case creds
when File
YAML.load_file(creds.path)
when String
YAML.load_file(creds)
when Hash
creds
else
raise ArgumentError, "Credentials are not a path, file, or hash."
end
end
private :find_credentials
end
# Store files in a database.
#
# Usage is identical to the file system storage version, except:
#
# 1. In your model specify the "database" storage option; for example:
# has_attached_file :avatar, :storage => :database
#
# 2. The file will be stored in a column called [attachment name]_file (e.g. "avatar_file") by default.
#
# To specify a different column name, use :column, like this:
# has_attached_file :avatar, :storage => :database, :column => 'avatar_data'
#
# If you have defined different styles, these files will be stored in additional columns called
# [attachment name]_[style name]_file (e.g. "avatar_thumb_file") by default.
#
# To specify different column names for styles, use :column in the style definition, like this:
# has_attached_file :avatar,
# :storage => :database,
# :styles => {
# :medium => {:geometry => "300x300>", :column => 'medium_file'},
# :thumb => {:geometry => "100x100>", :column => 'thumb_file'}
# }
#
# 3. You need to create these new columns in your migrations or you'll get an exception. Example:
# add_column :users, :avatar_file, :binary
# add_column :users, :avatar_medium_file, :binary
# add_column :users, :avatar_thumb_file, :binary
#
# Note the "binary" migration will not work for the LONGBLOB type in MySQL for the
# file_contents column. You may need to craft a SQL statement for your migration,
# depending on which database server you are using. Here's an example migration for MySQL:
# execute 'ALTER TABLE users ADD COLUMN avatar_file LONGBLOB'
# execute 'ALTER TABLE users ADD COLUMN avatar_medium_file LONGBLOB'
# execute 'ALTER TABLE users ADD COLUMN avatar_thumb_file LONGBLOB'
#
# 4. To avoid performance problems loading all of the BLOB columns every time you access
# your ActiveRecord object, a class method is provided on your model called
# “select_without_file_columns_for.” This is set to a :select scope hash that will
# instruct ActiveRecord::Base.find to load all of the columns except the BLOB/file data columns.
#
# If you’re using Rails 2.3, you can specify this as a default scope:
# default_scope select_without_file_columns_for(:avatar)
#
# Or if you’re using Rails 2.1 or 2.2 you can use it to create a named scope:
# named_scope :without_file_data, select_without_file_columns_for(:avatar)
#
# 5. By default, URLs will be set to this pattern:
# /:relative_root/:class/:attachment/:id?style=:style
#
# Example:
# /app-root-url/users/avatars/23?style=original
#
# The idea here is that to retrieve a file from the database storage, you will need some
# controller's code to be executed.
#
# Once you pick a controller to use for downloading, you can add this line
# to generate the download action for the default URL/action (the plural attachment name),
# "avatars" in this example:
# downloads_files_for :user, :avatar
#
# Or you can write a download method manually if there are security, logging or other
# requirements.
#
# If you prefer a different URL for downloading files you can specify that in the model; e.g.:
# has_attached_file :avatar, :storage => :database, :url => '/users/show_avatar/:id/:style'
#
# 6. Add a route for the download to the controller which will handle downloads, if necessary.
#
# The default URL, /:relative_root/:class/:attachment/:id?style=:style, will be matched by
# the default route: :controller/:action/:id
#
module Database
def self.extended base
base.instance_eval do
@file_columns = @options[:file_columns]
if @url == base.class.default_options[:url]
@url = ":relative_root/:class/:attachment/:id?style=:style"
end
end
base.class.interpolations[:relative_root] = lambda do |attachment, style|
begin
if ActionController::AbstractRequest.respond_to?(:relative_url_root)
relative_url_root = ActionController::AbstractRequest.relative_url_root
end
rescue NameError
end
if !relative_url_root && ActionController::Base.respond_to?(:relative_url_root)
relative_url_root = ActionController::Base.relative_url_root
end
relative_url_root
end
ActiveRecord::Base.logger.info("[paperclip] Database Storage Initalized.")
end
def column_for_style style
@file_columns[style.to_sym]
end
def instance_read_file(style)
column = column_for_style(style)
responds = instance.respond_to?(column)
cached = self.instance_variable_get("@_#{column}")
return cached if cached
# The blob attribute will not be present if select_without_file_columns_for was used
instance.reload :select => column if !instance.attribute_present?(column) && !instance.new_record?
instance.send(column) if responds
end
def instance_write_file(style, value)
setter = :"#{column_for_style(style)}="
responds = instance.respond_to?(setter)
self.instance_variable_set("@_#{setter.to_s.chop}", value)
instance.send(setter, value) if responds
end
def file_contents(style = default_style)
instance_read_file(style)
end
alias_method :data, :file_contents
def exists?(style = default_style)
!file_contents(style).nil?
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
if @queued_for_write[style]
@queued_for_write[style]
elsif exists?(style)
tempfile = Tempfile.new instance_read(:file_name)
tempfile.write file_contents(style)
tempfile
else
nil
end
end
alias_method :to_io, :to_file
def path style = default_style
original_filename.nil? ? nil : column_for_style(style)
end
def assign uploaded_file
# Assign standard metadata attributes and perform post processing as usual
super
# Save the file contents for all styles in ActiveRecord immediately (before save)
@queued_for_write.each do |style, file|
instance_write_file(style, file.read)
end
# If we are assigning another Paperclip attachment, then fixup the
# filename and content type; necessary since Tempfile is used in to_file
if uploaded_file.is_a?(Paperclip::Attachment)
instance_write(:file_name, uploaded_file.instance_read(:file_name))
instance_write(:content_type, uploaded_file.instance_read(:content_type))
end
end
def queue_existing_for_delete
[:original, *@styles.keys].uniq.each do |style|
instance_write_file(style, nil)
end
instance_write(:file_name, nil)
instance_write(:content_type, nil)
instance_write(:file_size, nil)
instance_write(:updated_at, nil)
end
def flush_writes
@queued_for_write = {}
end
def flush_deletes
@queued_for_delete = []
end
module ControllerClassMethods
def self.included(base)
base.extend(self)
end
def downloads_files_for(model, attachment)
define_method("#{attachment.to_s.pluralize}") do
model_record = Object.const_get(model.to_s.camelize.to_sym).find(params[:id])
style = params[:style] ? params[:style] : 'original'
send_data model_record.send(attachment).file_contents(style),
:filename => model_record.send("#{attachment}_file_name".to_sym),
:type => model_record.send("#{attachment}_content_type".to_sym)
end
end
end
end
end
end