jcnetdev-paperclip 1.0.20080704
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README +48 -0
- data/README.rdoc +48 -0
- data/Rakefile +84 -0
- data/generators/paperclip/USAGE +5 -0
- data/generators/paperclip/paperclip_generator.rb +27 -0
- data/generators/paperclip/templates/paperclip_migration.rb +17 -0
- data/init.rb +1 -0
- data/lib/paperclip.rb +239 -0
- data/lib/paperclip/attachment.rb +243 -0
- data/lib/paperclip/geometry.rb +109 -0
- data/lib/paperclip/iostream.rb +43 -0
- data/lib/paperclip/storage.rb +179 -0
- data/lib/paperclip/thumbnail.rb +80 -0
- data/lib/paperclip/upfile.rb +33 -0
- data/paperclip.gemspec +55 -0
- data/rails/init.rb +1 -0
- data/tasks/paperclip_tasks.rake +38 -0
- data/test/.gitignore +1 -0
- data/test/database.yml +5 -0
- data/test/fixtures/12k.png +0 -0
- data/test/fixtures/50x50.png +0 -0
- data/test/fixtures/5k.png +0 -0
- data/test/fixtures/bad.png +1 -0
- data/test/fixtures/text.txt +0 -0
- data/test/helper.rb +44 -0
- data/test/test_attachment.rb +286 -0
- data/test/test_geometry.rb +142 -0
- data/test/test_integration.rb +331 -0
- data/test/test_iostream.rb +60 -0
- data/test/test_paperclip.rb +123 -0
- data/test/test_storage.rb +136 -0
- data/test/test_thumbnail.rb +107 -0
- metadata +93 -0
@@ -0,0 +1,243 @@
|
|
1
|
+
module Paperclip
|
2
|
+
# The Attachment class manages the files for a given attachment. It saves when the model saves,
|
3
|
+
# deletes when the model is destroyed, and processes the file upon assignment.
|
4
|
+
class Attachment
|
5
|
+
|
6
|
+
def self.default_options
|
7
|
+
@default_options ||= {
|
8
|
+
:url => "/:attachment/:id/:style/:basename.:extension",
|
9
|
+
:path => ":rails_root/public/:attachment/:id/:style/:basename.:extension",
|
10
|
+
:styles => {},
|
11
|
+
:default_url => "/:attachment/:style/missing.png",
|
12
|
+
:default_style => :original,
|
13
|
+
:validations => [],
|
14
|
+
:storage => :filesystem
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :name, :instance, :styles, :default_style
|
19
|
+
|
20
|
+
# Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
|
21
|
+
# ActiveRecord object instance it's attached to, and +options+ is the same as the hash
|
22
|
+
# passed to +has_attached_file+.
|
23
|
+
def initialize name, instance, options = {}
|
24
|
+
@name = name
|
25
|
+
@instance = instance
|
26
|
+
|
27
|
+
options = self.class.default_options.merge(options)
|
28
|
+
|
29
|
+
@url = options[:url]
|
30
|
+
@path = options[:path]
|
31
|
+
@styles = options[:styles]
|
32
|
+
@default_url = options[:default_url]
|
33
|
+
@validations = options[:validations]
|
34
|
+
@default_style = options[:default_style]
|
35
|
+
@storage = options[:storage]
|
36
|
+
@whiny_thumbnails = options[:whiny_thumbnails]
|
37
|
+
@options = options
|
38
|
+
@queued_for_delete = []
|
39
|
+
@queued_for_write = {}
|
40
|
+
@errors = []
|
41
|
+
@validation_errors = nil
|
42
|
+
@dirty = false
|
43
|
+
|
44
|
+
normalize_style_definition
|
45
|
+
initialize_storage
|
46
|
+
end
|
47
|
+
|
48
|
+
# What gets called when you call instance.attachment = File. It clears errors,
|
49
|
+
# assigns attributes, processes the file, and runs validations. It also queues up
|
50
|
+
# the previous file for deletion, to be flushed away on #save of its host.
|
51
|
+
def assign uploaded_file
|
52
|
+
return nil unless valid_assignment?(uploaded_file)
|
53
|
+
|
54
|
+
queue_existing_for_delete
|
55
|
+
@errors = []
|
56
|
+
@validation_errors = nil
|
57
|
+
|
58
|
+
return nil if uploaded_file.nil?
|
59
|
+
|
60
|
+
@queued_for_write[:original] = uploaded_file.to_tempfile
|
61
|
+
@instance[:"#{@name}_file_name"] = uploaded_file.original_filename.strip.gsub /[^\w\d\.\-]+/, '_'
|
62
|
+
@instance[:"#{@name}_content_type"] = uploaded_file.content_type.strip
|
63
|
+
@instance[:"#{@name}_file_size"] = uploaded_file.size.to_i
|
64
|
+
|
65
|
+
@dirty = true
|
66
|
+
|
67
|
+
post_process
|
68
|
+
ensure
|
69
|
+
validate
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the public URL of the attachment, with a given style. Note that this
|
73
|
+
# does not necessarily need to point to a file that your web server can access
|
74
|
+
# and can point to an action in your app, if you need fine grained security.
|
75
|
+
# This is not recommended if you don't need the security, however, for
|
76
|
+
# performance reasons.
|
77
|
+
def url style = default_style
|
78
|
+
original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the path of the attachment as defined by the :path optionn. If the
|
82
|
+
# file is stored in the filesystem the path refers to the path of the file on
|
83
|
+
# disk. If the file is stored in S3, the path is the "key" part of th URL,
|
84
|
+
# and the :bucket option refers to the S3 bucket.
|
85
|
+
def path style = nil #:nodoc:
|
86
|
+
interpolate(@path, style)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Alias to +url+
|
90
|
+
def to_s style = nil
|
91
|
+
url(style)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns true if there are any errors on this attachment.
|
95
|
+
def valid?
|
96
|
+
errors.length == 0
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns an array containing the errors on this attachment.
|
100
|
+
def errors
|
101
|
+
@errors.compact.uniq
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if there are changes that need to be saved.
|
105
|
+
def dirty?
|
106
|
+
@dirty
|
107
|
+
end
|
108
|
+
|
109
|
+
# Saves the file, if there are no errors. If there are, it flushes them to
|
110
|
+
# the instance's errors and returns false, cancelling the save.
|
111
|
+
def save
|
112
|
+
if valid?
|
113
|
+
flush_deletes
|
114
|
+
flush_writes
|
115
|
+
@dirty = false
|
116
|
+
true
|
117
|
+
else
|
118
|
+
flush_errors
|
119
|
+
false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the name of the file as originally assigned, and as lives in the
|
124
|
+
# <attachment>_file_name attribute of the model.
|
125
|
+
def original_filename
|
126
|
+
instance[:"#{name}_file_name"]
|
127
|
+
end
|
128
|
+
|
129
|
+
# A hash of procs that are run during the interpolation of a path or url.
|
130
|
+
# A variable of the format :name will be replaced with the return value of
|
131
|
+
# the proc named ":name". Each lambda takes the attachment and the current
|
132
|
+
# style as arguments. This hash can be added to with your own proc if
|
133
|
+
# necessary.
|
134
|
+
def self.interpolations
|
135
|
+
@interpolations ||= {
|
136
|
+
:rails_root => lambda{|attachment,style| RAILS_ROOT },
|
137
|
+
:class => lambda do |attachment,style|
|
138
|
+
attachment.instance.class.name.underscore.pluralize
|
139
|
+
end,
|
140
|
+
:basename => lambda do |attachment,style|
|
141
|
+
attachment.original_filename.gsub(File.extname(attachment.original_filename), "")
|
142
|
+
end,
|
143
|
+
:extension => lambda do |attachment,style|
|
144
|
+
((style = attachment.styles[style]) && style.last) ||
|
145
|
+
File.extname(attachment.original_filename).gsub(/^\.+/, "")
|
146
|
+
end,
|
147
|
+
:id => lambda{|attachment,style| attachment.instance.id },
|
148
|
+
:id_partition => lambda do |attachment, style|
|
149
|
+
("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
|
150
|
+
end,
|
151
|
+
:attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
|
152
|
+
:style => lambda{|attachment,style| style || attachment.default_style },
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
# This method really shouldn't be called that often. It's expected use is in the
|
157
|
+
# paperclip:refresh rake task and that's it. It will regenerate all thumbnails
|
158
|
+
# forcefully, by reobtaining the original file and going through the post-process
|
159
|
+
# again.
|
160
|
+
def reprocess!
|
161
|
+
new_original = Tempfile.new("paperclip-reprocess")
|
162
|
+
old_original = to_file(:original)
|
163
|
+
new_original.write( old_original.read )
|
164
|
+
new_original.rewind
|
165
|
+
|
166
|
+
@queued_for_write = { :original => new_original }
|
167
|
+
post_process
|
168
|
+
|
169
|
+
old_original.close if old_original.respond_to?(:close)
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def valid_assignment? file #:nodoc:
|
175
|
+
file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
|
176
|
+
end
|
177
|
+
|
178
|
+
def validate #:nodoc:
|
179
|
+
unless @validation_errors
|
180
|
+
@validation_errors = @validations.collect do |v|
|
181
|
+
v.call(self, instance)
|
182
|
+
end.flatten.compact.uniq
|
183
|
+
@errors += @validation_errors
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def normalize_style_definition
|
188
|
+
@styles.each do |name, args|
|
189
|
+
dimensions, format = [args, nil].flatten[0..1]
|
190
|
+
format = nil if format == ""
|
191
|
+
@styles[name] = [dimensions, format]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def initialize_storage
|
196
|
+
@storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
|
197
|
+
self.extend(@storage_module)
|
198
|
+
end
|
199
|
+
|
200
|
+
def post_process #:nodoc:
|
201
|
+
return if @queued_for_write[:original].nil?
|
202
|
+
@styles.each do |name, args|
|
203
|
+
begin
|
204
|
+
dimensions, format = args
|
205
|
+
@queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
|
206
|
+
dimensions,
|
207
|
+
format,
|
208
|
+
@whiny_thumnails)
|
209
|
+
rescue PaperclipError => e
|
210
|
+
@errors << e.message if @whiny_thumbnails
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def interpolate pattern, style = default_style #:nodoc:
|
216
|
+
interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
|
217
|
+
interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
|
218
|
+
tag, blk = interpolation
|
219
|
+
result.gsub(/:#{tag}/) do |match|
|
220
|
+
blk.call( self, style )
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def queue_existing_for_delete #:nodoc:
|
226
|
+
return if original_filename.blank?
|
227
|
+
@queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
|
228
|
+
path(style) if exists?(style)
|
229
|
+
end.compact
|
230
|
+
@instance[:"#{@name}_file_name"] = nil
|
231
|
+
@instance[:"#{@name}_content_type"] = nil
|
232
|
+
@instance[:"#{@name}_file_size"] = nil
|
233
|
+
end
|
234
|
+
|
235
|
+
def flush_errors #:nodoc:
|
236
|
+
@errors.each do |error|
|
237
|
+
instance.errors.add(name, error)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Paperclip
|
2
|
+
|
3
|
+
# Defines the geometry of an image.
|
4
|
+
class Geometry
|
5
|
+
attr_accessor :height, :width, :modifier
|
6
|
+
|
7
|
+
# Gives a Geometry representing the given height and width
|
8
|
+
def initialize width = nil, height = nil, modifier = nil
|
9
|
+
height = nil if height == ""
|
10
|
+
width = nil if width == ""
|
11
|
+
@height = (height || width).to_f
|
12
|
+
@width = (width || height).to_f
|
13
|
+
@modifier = modifier
|
14
|
+
end
|
15
|
+
|
16
|
+
# Uses ImageMagick to determing the dimensions of a file, passed in as either a
|
17
|
+
# File or path.
|
18
|
+
def self.from_file file
|
19
|
+
file = file.path if file.respond_to? "path"
|
20
|
+
parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
|
21
|
+
raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parses a "WxH" formatted string, where W is the width and H is the height.
|
25
|
+
def self.parse string
|
26
|
+
if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
|
27
|
+
Geometry.new(*match[1,3])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# True if the dimensions represent a square
|
32
|
+
def square?
|
33
|
+
height == width
|
34
|
+
end
|
35
|
+
|
36
|
+
# True if the dimensions represent a horizontal rectangle
|
37
|
+
def horizontal?
|
38
|
+
height < width
|
39
|
+
end
|
40
|
+
|
41
|
+
# True if the dimensions represent a vertical rectangle
|
42
|
+
def vertical?
|
43
|
+
height > width
|
44
|
+
end
|
45
|
+
|
46
|
+
# The aspect ratio of the dimensions.
|
47
|
+
def aspect
|
48
|
+
width / height
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the larger of the two dimensions
|
52
|
+
def larger
|
53
|
+
[height, width].max
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the smaller of the two dimensions
|
57
|
+
def smaller
|
58
|
+
[height, width].min
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the width and height in a format suitable to be passed to Geometry.parse
|
62
|
+
def to_s
|
63
|
+
"%dx%d%s" % [width, height, modifier]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Same as to_s
|
67
|
+
def inspect
|
68
|
+
to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the scaling and cropping geometries (in string-based ImageMagick format)
|
72
|
+
# neccessary to transform this Geometry into the Geometry given. If crop is true,
|
73
|
+
# then it is assumed the destination Geometry will be the exact final resolution.
|
74
|
+
# In this case, the source Geometry is scaled so that an image containing the
|
75
|
+
# destination Geometry would be completely filled by the source image, and any
|
76
|
+
# overhanging image would be cropped. Useful for square thumbnail images. The cropping
|
77
|
+
# is weighted at the center of the Geometry.
|
78
|
+
def transformation_to dst, crop = false
|
79
|
+
ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
|
80
|
+
|
81
|
+
if crop
|
82
|
+
scale_geometry, scale = scaling(dst, ratio)
|
83
|
+
crop_geometry = cropping(dst, ratio, scale)
|
84
|
+
else
|
85
|
+
scale_geometry = dst.to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
[ scale_geometry, crop_geometry ]
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def scaling dst, ratio
|
94
|
+
if ratio.horizontal? || ratio.square?
|
95
|
+
[ "%dx" % dst.width, ratio.width ]
|
96
|
+
else
|
97
|
+
[ "x%d" % dst.height, ratio.height ]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def cropping dst, ratio, scale
|
102
|
+
if ratio.horizontal? || ratio.square?
|
103
|
+
"%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
|
104
|
+
else
|
105
|
+
"%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
|
2
|
+
# and Tempfile conversion.
|
3
|
+
module IOStream
|
4
|
+
|
5
|
+
# Returns a Tempfile containing the contents of the readable object.
|
6
|
+
def to_tempfile
|
7
|
+
tempfile = Tempfile.new("stream")
|
8
|
+
tempfile.binmode
|
9
|
+
self.stream_to(tempfile)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Copies one read-able object from one place to another in blocks, obviating the need to load
|
13
|
+
# the whole thing into memory. Defaults to 8k blocks. If this module is included in both
|
14
|
+
# both StringIO and Tempfile, then either can have its data copied anywhere else without typing
|
15
|
+
# worries or memory overhead worries. Returns a File if a String is passed in as the destination
|
16
|
+
# and returns the IO or Tempfile as passed in if one is sent as the destination.
|
17
|
+
def stream_to path_or_file, in_blocks_of = 8192
|
18
|
+
dstio = case path_or_file
|
19
|
+
when String then File.new(path_or_file, "wb+")
|
20
|
+
when IO then path_or_file
|
21
|
+
when Tempfile then path_or_file
|
22
|
+
end
|
23
|
+
buffer = ""
|
24
|
+
self.rewind
|
25
|
+
while self.read(in_blocks_of, buffer) do
|
26
|
+
dstio.write(buffer)
|
27
|
+
end
|
28
|
+
dstio.rewind
|
29
|
+
dstio
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class IO
|
34
|
+
include IOStream
|
35
|
+
end
|
36
|
+
|
37
|
+
%w( Tempfile StringIO ).each do |klass|
|
38
|
+
if Object.const_defined? klass
|
39
|
+
Object.const_get(klass).class_eval do
|
40
|
+
include IOStream
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Paperclip
|
2
|
+
module Storage
|
3
|
+
|
4
|
+
# The default place to store attachments is in the filesystem. Files on the local
|
5
|
+
# filesystem can be very easily served by Apache without requiring a hit to your app.
|
6
|
+
# They also can be processed more easily after they've been saved, as they're just
|
7
|
+
# normal files. There is one Filesystem-specific option for has_attached_file.
|
8
|
+
# * +path+: The location of the repository of attachments on disk. This can (and, in
|
9
|
+
# almost all cases, should) be coordinated with the value of the +url+ option to
|
10
|
+
# allow files to be saved into a place where Apache can serve them without
|
11
|
+
# hitting your app. Defaults to
|
12
|
+
# ":rails_root/public/:class/:attachment/:id/:style_:filename".
|
13
|
+
# By default this places the files in the app's public directory which can be served
|
14
|
+
# directly. If you are using capistrano for deployment, a good idea would be to
|
15
|
+
# make a symlink to the capistrano-created system directory from inside your app's
|
16
|
+
# public directory.
|
17
|
+
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
|
18
|
+
# :path => "/var/app/attachments/:class/:id/:style/:filename"
|
19
|
+
module Filesystem
|
20
|
+
def self.extended base
|
21
|
+
end
|
22
|
+
|
23
|
+
def exists?(style = default_style)
|
24
|
+
if original_filename
|
25
|
+
File.exist?(path(style))
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns representation of the data of the file assigned to the given
|
32
|
+
# style, in the format most representative of the current storage.
|
33
|
+
def to_file style = default_style
|
34
|
+
@queued_for_write[style] || (File.new(path(style)) if exists?(style))
|
35
|
+
end
|
36
|
+
alias_method :to_io, :to_file
|
37
|
+
|
38
|
+
def flush_writes #:nodoc:
|
39
|
+
@queued_for_write.each do |style, file|
|
40
|
+
FileUtils.mkdir_p(File.dirname(path(style)))
|
41
|
+
result = file.stream_to(path(style))
|
42
|
+
file.close
|
43
|
+
result.close
|
44
|
+
end
|
45
|
+
@queued_for_write = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def flush_deletes #:nodoc:
|
49
|
+
@queued_for_delete.each do |path|
|
50
|
+
begin
|
51
|
+
FileUtils.rm(path) if File.exist?(path)
|
52
|
+
rescue Errno::ENOENT => e
|
53
|
+
# ignore file-not-found, let everything else pass
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@queued_for_delete = []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Amazon's S3 file hosting service is a scalable, easy place to store files for
|
61
|
+
# distribution. You can find out more about it at http://aws.amazon.com/s3
|
62
|
+
# There are a few S3-specific options for has_attached_file:
|
63
|
+
# * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
|
64
|
+
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
|
65
|
+
# gives you. You can 'environment-space' this just like you do to your
|
66
|
+
# database.yml file, so different environments can use different accounts:
|
67
|
+
# development:
|
68
|
+
# access_key_id: 123...
|
69
|
+
# secret_access_key: 123...
|
70
|
+
# test:
|
71
|
+
# access_key_id: abc...
|
72
|
+
# secret_access_key: abc...
|
73
|
+
# production:
|
74
|
+
# access_key_id: 456...
|
75
|
+
# secret_access_key: 456...
|
76
|
+
# This is not required, however, and the file may simply look like this:
|
77
|
+
# access_key_id: 456...
|
78
|
+
# secret_access_key: 456...
|
79
|
+
# In which case, those access keys will be used in all environments.
|
80
|
+
# * +s3_permissions+: This is a String that should be one of the "canned" access
|
81
|
+
# policies that S3 provides (more information can be found here:
|
82
|
+
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
|
83
|
+
# The default for Paperclip is "public-read".
|
84
|
+
# * +bucket+: This is the name of the S3 bucket that will store your files. Remember
|
85
|
+
# that the bucket must be unique across all of Amazon S3. If the bucket does not exist
|
86
|
+
# Paperclip will attempt to create it. The bucket name will not be interpolated.
|
87
|
+
# * +path+: This is the key under the bucket in which the file will be stored. The
|
88
|
+
# URL will be constructed from the bucket and the path. This is what you will want
|
89
|
+
# to interpolate. Keys should be unique, like filenames, and despite the fact that
|
90
|
+
# S3 (strictly speaking) does not support directories, you can still use a / to
|
91
|
+
# separate parts of your file name.
|
92
|
+
module S3
|
93
|
+
def self.extended base
|
94
|
+
require 'right_aws'
|
95
|
+
base.instance_eval do
|
96
|
+
@bucket = @options[:bucket]
|
97
|
+
@s3_credentials = parse_credentials(@options[:s3_credentials])
|
98
|
+
@s3_options = @options[:s3_options] || {}
|
99
|
+
@s3_permissions = @options[:s3_permissions] || 'public-read'
|
100
|
+
@url = ":s3_url"
|
101
|
+
end
|
102
|
+
base.class.interpolations[:s3_url] = lambda do |attachment, style|
|
103
|
+
"https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def s3
|
108
|
+
@s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
|
109
|
+
@s3_credentials[:secret_access_key],
|
110
|
+
@s3_options)
|
111
|
+
end
|
112
|
+
|
113
|
+
def s3_bucket
|
114
|
+
@s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
|
115
|
+
end
|
116
|
+
|
117
|
+
def bucket_name
|
118
|
+
@bucket
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_credentials creds
|
122
|
+
creds = find_credentials(creds).stringify_keys
|
123
|
+
(creds[ENV['RAILS_ENV']] || creds).symbolize_keys
|
124
|
+
end
|
125
|
+
|
126
|
+
def exists?(style = default_style)
|
127
|
+
s3_bucket.key(path(style)) ? true : false
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns representation of the data of the file assigned to the given
|
131
|
+
# style, in the format most representative of the current storage.
|
132
|
+
def to_file style = default_style
|
133
|
+
@queued_for_write[style] || s3_bucket.key(path(style))
|
134
|
+
end
|
135
|
+
alias_method :to_io, :to_file
|
136
|
+
|
137
|
+
def flush_writes #:nodoc:
|
138
|
+
@queued_for_write.each do |style, file|
|
139
|
+
begin
|
140
|
+
key = s3_bucket.key(path(style))
|
141
|
+
key.data = file
|
142
|
+
key.put(nil, @s3_permissions)
|
143
|
+
rescue RightAws::AwsError => e
|
144
|
+
raise
|
145
|
+
end
|
146
|
+
end
|
147
|
+
@queued_for_write = {}
|
148
|
+
end
|
149
|
+
|
150
|
+
def flush_deletes #:nodoc:
|
151
|
+
@queued_for_delete.each do |path|
|
152
|
+
begin
|
153
|
+
if file = s3_bucket.key(path)
|
154
|
+
file.delete
|
155
|
+
end
|
156
|
+
rescue RightAws::AwsError
|
157
|
+
# Ignore this.
|
158
|
+
end
|
159
|
+
end
|
160
|
+
@queued_for_delete = []
|
161
|
+
end
|
162
|
+
|
163
|
+
def find_credentials creds
|
164
|
+
case creds
|
165
|
+
when File:
|
166
|
+
YAML.load_file(creds.path)
|
167
|
+
when String:
|
168
|
+
YAML.load_file(creds)
|
169
|
+
when Hash:
|
170
|
+
creds
|
171
|
+
else
|
172
|
+
raise ArgumentError, "Credentials are not a path, file, or hash."
|
173
|
+
end
|
174
|
+
end
|
175
|
+
private :find_credentials
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|