attachment_zen 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,394 @@
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
+ # If you wish to use Amazon CloudFront to serve the files, you can also specify a distibution domain for the bucket.
21
+ # To read more about CloudFront, visit http://aws.amazon.com/cloudfront
22
+ #
23
+ # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
24
+ #
25
+ # development:
26
+ # bucket_name: appname_development
27
+ # access_key_id: <your key>
28
+ # secret_access_key: <your key>
29
+ # distribution_domain: XXXX.cloudfront.net
30
+ #
31
+ # test:
32
+ # bucket_name: appname_test
33
+ # access_key_id: <your key>
34
+ # secret_access_key: <your key>
35
+ # distribution_domain: XXXX.cloudfront.net
36
+ #
37
+ # production:
38
+ # bucket_name: appname
39
+ # access_key_id: <your key>
40
+ # secret_access_key: <your key>
41
+ # distribution_domain: XXXX.cloudfront.net
42
+ #
43
+ # You can change the location of the config path by passing a full path to the :s3_config_path option.
44
+ #
45
+ # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
46
+ #
47
+ # === Required configuration parameters
48
+ #
49
+ # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
50
+ # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
51
+ # * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
52
+ #
53
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
54
+ #
55
+ # == About bucket names
56
+ #
57
+ # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
58
+ # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
59
+ # implementation to the development, test, and production environments.
60
+ #
61
+ # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
62
+ #
63
+ # === Optional configuration parameters
64
+ #
65
+ # * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
66
+ # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
67
+ # * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
68
+ # * <tt>:distribution_domain</tt> - The CloudFront distribution domain for the bucket. This can either be the assigned
69
+ # distribution domain (ie. XXX.cloudfront.net) or a chosen domain using a CNAME. See CloudFront for more details.
70
+ #
71
+ # == Usage
72
+ #
73
+ # To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
74
+ #
75
+ # class Photo < ActiveRecord::Base
76
+ # has_attachment :storage => :s3
77
+ # end
78
+ #
79
+ # === Customizing the path
80
+ #
81
+ # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
82
+ # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
83
+ # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
84
+ # option:
85
+ #
86
+ # class Photo < ActiveRecord::Base
87
+ # has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
88
+ # end
89
+ #
90
+ # Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
91
+ #
92
+ # === Using different bucket names on different models
93
+ #
94
+ # By default the bucket name that the file will be stored to is the one specified by the
95
+ # <tt>:bucket_name</tt> key in the amazon_s3.yml file. You can use the <tt>:bucket_key</tt> option
96
+ # to overide this behavior on a per model basis. For instance if you want a bucket that will hold
97
+ # only Photos you can do this:
98
+ #
99
+ # class Photo < ActiveRecord::Base
100
+ # has_attachment :storage => :s3, :bucket_key => :photo_bucket_name
101
+ # end
102
+ #
103
+ # And then your amazon_s3.yml file needs to look like this.
104
+ #
105
+ # development:
106
+ # bucket_name: appname_development
107
+ # access_key_id: <your key>
108
+ # secret_access_key: <your key>
109
+ #
110
+ # test:
111
+ # bucket_name: appname_test
112
+ # access_key_id: <your key>
113
+ # secret_access_key: <your key>
114
+ #
115
+ # production:
116
+ # bucket_name: appname
117
+ # photo_bucket_name: appname_photos
118
+ # access_key_id: <your key>
119
+ # secret_access_key: <your key>
120
+ #
121
+ # If the bucket_key you specify is not there in a certain environment then attachment_fu will
122
+ # default to the <tt>bucket_name</tt> key. This way you only have to create special buckets
123
+ # this can be helpful if you only need special buckets in certain environments.
124
+ #
125
+ # === Permissions
126
+ #
127
+ # By default, files are stored on S3 with public access permissions. You can customize this using
128
+ # the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are
129
+ # <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
130
+ #
131
+ # === Other options
132
+ #
133
+ # Of course, all the usual configuration options apply, such as content_type and thumbnails:
134
+ #
135
+ # class Photo < ActiveRecord::Base
136
+ # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
137
+ # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
138
+ # end
139
+ #
140
+ # === Accessing S3 URLs
141
+ #
142
+ # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
143
+ # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
144
+ #
145
+ # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
146
+ #
147
+ # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
148
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
149
+ #
150
+ # Additionally, you can get an object's base path relative to the bucket root using
151
+ # <tt>base_path</tt>:
152
+ #
153
+ # @photo.file_base_path # => photos/1
154
+ #
155
+ # And the full path (including the filename) using <tt>full_filename</tt>:
156
+ #
157
+ # @photo.full_filename # => photos/
158
+ #
159
+ # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
160
+ # You can retrieve the bucket name using the <tt>bucket_name</tt> method.
161
+ #
162
+ # === Accessing CloudFront URLs
163
+ #
164
+ # You can get an object's CloudFront URL using the cloudfront_url accessor. Using the example from above:
165
+ # @postcard.cloudfront_url # => http://XXXX.cloudfront.net/photos/1/mexico.jpg
166
+ #
167
+ # The resulting url is in the form: http://:distribution_domain/:table_name/:id/:file
168
+ #
169
+ # If you set :cloudfront to true in your model, the public_url will be the CloudFront
170
+ # URL, not the S3 URL.
171
+ class S3Backend < BackendDelegator
172
+ class RequiredLibraryNotFoundError < StandardError; end
173
+ class ConfigFileNotFoundError < StandardError; end
174
+
175
+ attr_accessor :s3_config
176
+ attr_reader :bucket_name
177
+
178
+ @@s3_config_path = nil
179
+
180
+ def initialize(obj, opts)
181
+ # zendesk classic rails usage note
182
+ # all the options from our attachments.yml come in through the opts argument here.
183
+ super(obj, opts)
184
+
185
+ self.s3_config = if @@s3_config_path
186
+ # config from file.
187
+ YAML.load(ERB.new(File.read(@@s3_config_path)).result).fetch(ENV['RAILS_ENV']).symbolize_keys
188
+ else
189
+ # config entirely through initializer
190
+
191
+ # a few options need renaming.
192
+ opts[:access_key_id] = opts[:s3_access_key] if opts.has_key? :s3_access_key
193
+ opts[:secret_access_key] = opts[:s3_secret_key] if opts.has_key? :s3_secret_key
194
+
195
+ opts
196
+ end
197
+
198
+ # :use_ssl defaults to true now in AWS::SDK
199
+ # the rest of our code relies on checking for this value in s3_config
200
+ s3_config[:use_ssl] = true unless s3_config.has_key?(:use_ssl)
201
+
202
+ @bucket_name = self.s3_config[:bucket_name]
203
+
204
+ end
205
+
206
+ def self.included_in_base(base) #:nodoc:
207
+
208
+ begin
209
+ require 'aws-sdk-v1'
210
+ rescue LoadError
211
+ raise RequiredLibraryNotFoundError.new('AWS::SDK could not be loaded')
212
+ end
213
+
214
+ if base.attachment_options[:s3_config_path]
215
+ @@s3_config_path = base.attachment_options[:s3_config_path] || Rails.root.join('config/amazon_s3.yml').to_s
216
+ end
217
+
218
+ end
219
+
220
+ def connection
221
+ @s3 ||= AWS::S3.new(s3_config)
222
+ end
223
+
224
+ def protocol
225
+ @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
226
+ end
227
+
228
+ def hostname
229
+ connection.client.endpoint
230
+ end
231
+
232
+ def port_string
233
+ connection.client.port.to_s
234
+ end
235
+
236
+ def distribution_domain
237
+ @distribution_domain = s3_config[:distribution_domain]
238
+ end
239
+ def s3_protocol
240
+ protocol
241
+ end
242
+
243
+ def s3_hostname
244
+ hostname
245
+ end
246
+
247
+ def s3_port_string
248
+ port_string
249
+ end
250
+
251
+ def cloudfront_distribution_domain
252
+ distribution_domain
253
+ end
254
+
255
+ # called by the ActiveRecord class from filename=
256
+ def notify_rename
257
+ @old_filename = filename unless filename.nil? || @old_filename
258
+ end
259
+
260
+ # The attachment ID used in the full path of a file
261
+ def attachment_path_id
262
+ ((respond_to?(:parent_id) && parent_id) || @obj.id).to_s
263
+ end
264
+
265
+ # The pseudo hierarchy containing the file relative to the bucket name
266
+ # Example: <tt>:table_name/:id</tt>
267
+ def base_path
268
+ File.join(attachment_options[:path_prefix], attachment_path_id)
269
+ end
270
+
271
+ # The full path to the file relative to the bucket name
272
+ # Example: <tt>:table_name/:id/:filename</tt>
273
+ def full_filename(thumbnail = nil)
274
+ File.join(base_path, thumbnail_name_for(thumbnail))
275
+ end
276
+
277
+ # All public objects are accessible via a GET request to the S3 servers. You can generate a
278
+ # url for an object using the s3_url method.
279
+ #
280
+ # @photo.s3_url
281
+ #
282
+ # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
283
+ # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
284
+ # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
285
+ #
286
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
287
+ def s3_url(thumbnail = nil)
288
+ # leave out the port if redundant
289
+ if ( s3_config[:use_ssl] && s3_port_string.to_s == '443' ) || ( ! s3_config[:use_ssl] && s3_port_string.to_s == '80' )
290
+ port_string = ''
291
+ else
292
+ port_string = ':' + s3_port_string
293
+ end
294
+ File.join(s3_protocol + bucket_name + '.' + s3_hostname + port_string, full_filename(thumbnail))
295
+ end
296
+
297
+ # All public objects are accessible via a GET request to CloudFront. You can generate a
298
+ # url for an object using the cloudfront_url method.
299
+ #
300
+ # @photo.cloudfront_url
301
+ #
302
+ # The resulting url is in the form: <tt>http://:distribution_domain/:table_name/:id/:file</tt> using
303
+ # the <tt>:distribution_domain</tt> variable set in the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
304
+ #
305
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
306
+ def cloudfront_url(thumbnail = nil)
307
+ "http://" + s3_config[:distribution_domain] + "/" + full_filename(thumbnail)
308
+ end
309
+
310
+ def public_url(*args)
311
+ if attachment_options[:cloudfront]
312
+ cloudfront_url(args)
313
+ else
314
+ s3_url(args)
315
+ end
316
+ end
317
+
318
+ # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
319
+ # authenticated url for an object like this:
320
+ #
321
+ # @photo.authenticated_s3_url
322
+ #
323
+ # By default authenticated urls expire 5 minutes after they were generated.
324
+ #
325
+ # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
326
+ # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
327
+ #
328
+ # # Absolute expiration date (October 13th, 2025)
329
+ # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
330
+ #
331
+ # # Expiration in five hours from now
332
+ # @photo.authenticated_s3_url(:expires_in => 5.hours)
333
+ #
334
+ # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
335
+ # By default, the ssl settings for the current connection will be used:
336
+ #
337
+ # @photo.authenticated_s3_url(:use_ssl => true)
338
+ #
339
+ # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
340
+ #
341
+ # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
342
+ def authenticated_s3_url(*args)
343
+ options = args.extract_options!
344
+ options[:expires_in] = options[:expires_in].to_i if options[:expires_in]
345
+ thumbnail = args.shift
346
+ self.bucket.objects[full_filename(thumbnail)].url_for(:get,options).to_s
347
+ end
348
+
349
+ def bucket
350
+ @bucket_c ||= connection.buckets[bucket_name]
351
+ end
352
+
353
+ def current_data
354
+ bucket.objects[full_filename].read
355
+ end
356
+
357
+ # Called in the after_destroy callback
358
+ def destroy_file
359
+ bucket.objects[full_filename].delete
360
+ rescue Errno::ECONNRESET
361
+ retries ||= 0
362
+ retries += 1
363
+ retry if retries <= 1
364
+ end
365
+
366
+ def rename_file
367
+ return unless @old_filename && @old_filename != filename
368
+
369
+ old_full_filename = File.join(base_path, @old_filename)
370
+
371
+ o = bucket.objects[old_full_filename]
372
+ o.rename_to(full_filename)
373
+
374
+ @old_filename = nil
375
+ true
376
+ end
377
+
378
+ def save_to_storage
379
+ if save_attachment?
380
+ obj = bucket.objects[full_filename]
381
+ if temp_path
382
+ obj.write(:file => temp_path, :content_type => content_type, :server_side_encryption => :aes256)
383
+ else
384
+ obj.write(temp_data, :content_type => content_type, :server_side_encryption => :aes256)
385
+ end
386
+ end
387
+
388
+ @old_filename = nil
389
+ true
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,93 @@
1
+ # This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry.
2
+ # Use #new_dimensions_for to get new dimensons
3
+ # Used so I can use spiffy RMagick geometry strings with ImageScience
4
+ class Technoweenie::AttachmentFu::Geometry
5
+ # ! and @ are removed until support for them is added
6
+ FLAGS = ['', '%', '<', '>']#, '!', '@']
7
+ RFLAGS = { '%' => :percent,
8
+ '!' => :aspect,
9
+ '<' => :>,
10
+ '>' => :<,
11
+ '@' => :area }
12
+
13
+ attr_accessor :width, :height, :x, :y, :flag
14
+
15
+ def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
16
+ # Support floating-point width and height arguments so Geometry
17
+ # objects can be used to specify Image#density= arguments.
18
+ raise ArgumentError, "width must be >= 0: #{width}" if width < 0
19
+ raise ArgumentError, "height must be >= 0: #{height}" if height < 0
20
+ @width = width.to_f
21
+ @height = height.to_f
22
+ @x = x.to_i
23
+ @y = y.to_i
24
+ @flag = flag
25
+ end
26
+
27
+ # Construct an object from a geometry string
28
+ RE = /\A(\d*)(?:x(\d+)?)?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/
29
+
30
+ def self.from_s(str)
31
+ raise(ArgumentError, "no geometry string specified") unless str
32
+
33
+ if m = RE.match(str)
34
+ new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]])
35
+ else
36
+ raise ArgumentError, "invalid geometry format"
37
+ end
38
+ end
39
+
40
+ # Convert object to a geometry string
41
+ def to_s
42
+ str = ''
43
+ str << "%g" % @width if @width > 0
44
+ str << 'x' if (@width > 0 || @height > 0)
45
+ str << "%g" % @height if @height > 0
46
+ str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
47
+ str << FLAGS[@flag.to_i]
48
+ end
49
+
50
+ # attempts to get new dimensions for the current geometry string given these old dimensions.
51
+ # This doesn't implement the aspect flag (!) or the area flag (@). PDI
52
+ def new_dimensions_for(orig_width, orig_height)
53
+ new_width = orig_width
54
+ new_height = orig_height
55
+
56
+ case @flag
57
+ when :percent
58
+ scale_x = @width.zero? ? 100 : @width
59
+ scale_y = @height.zero? ? @width : @height
60
+ new_width = scale_x.to_f * (orig_width.to_f / 100.0)
61
+ new_height = scale_y.to_f * (orig_height.to_f / 100.0)
62
+ when :<, :>, nil
63
+ scale_factor =
64
+ if new_width.zero? || new_height.zero?
65
+ 1.0
66
+ else
67
+ if @width.nonzero? && @height.nonzero?
68
+ [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min
69
+ else
70
+ @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f)
71
+ end
72
+ end
73
+ new_width = scale_factor * new_width.to_f
74
+ new_height = scale_factor * new_height.to_f
75
+ new_width = orig_width if @flag && orig_width.send(@flag, new_width)
76
+ new_height = orig_height if @flag && orig_height.send(@flag, new_height)
77
+ end
78
+
79
+ [new_width, new_height].collect! { |v| [v.round, 1].max }
80
+ end
81
+ end
82
+
83
+ class Array
84
+ # allows you to get new dimensions for the current array of dimensions with a given geometry string
85
+ #
86
+ # [50, 64] / '40>' # => [40, 51]
87
+ def /(geometry)
88
+ raise ArgumentError, "Only works with a [width, height] pair" if size != 2
89
+ raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Technoweenie::AttachmentFu::Geometry)
90
+ geometry = Technoweenie::AttachmentFu::Geometry.from_s(geometry) if geometry.is_a?(String)
91
+ geometry.new_dimensions_for first, last
92
+ end
93
+ end