tdd-attachment_fu 0.9.6

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.
Files changed (49) hide show
  1. data/CHANGELOG +44 -0
  2. data/LICENSE +20 -0
  3. data/README +197 -0
  4. data/Rakefile +22 -0
  5. data/amazon_s3.yml.tpl +17 -0
  6. data/init.rb +16 -0
  7. data/install.rb +7 -0
  8. data/lib/geometry.rb +96 -0
  9. data/lib/technoweenie/attachment_fu/backends/cloud_file_backend.rb +211 -0
  10. data/lib/technoweenie/attachment_fu/backends/db_file_backend.rb +39 -0
  11. data/lib/technoweenie/attachment_fu/backends/file_system_backend.rb +126 -0
  12. data/lib/technoweenie/attachment_fu/backends/s3_backend.rb +394 -0
  13. data/lib/technoweenie/attachment_fu/processors/core_image_processor.rb +65 -0
  14. data/lib/technoweenie/attachment_fu/processors/gd2_processor.rb +62 -0
  15. data/lib/technoweenie/attachment_fu/processors/image_science_processor.rb +80 -0
  16. data/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb +138 -0
  17. data/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb +65 -0
  18. data/lib/technoweenie/attachment_fu.rb +514 -0
  19. data/rackspace_cloudfiles.yml.tpl +14 -0
  20. data/test/backends/db_file_test.rb +16 -0
  21. data/test/backends/file_system_test.rb +143 -0
  22. data/test/backends/remote/cloudfiles_test.rb +102 -0
  23. data/test/backends/remote/s3_test.rb +119 -0
  24. data/test/base_attachment_tests.rb +77 -0
  25. data/test/basic_test.rb +70 -0
  26. data/test/database.yml +18 -0
  27. data/test/extra_attachment_test.rb +67 -0
  28. data/test/fixtures/attachment.rb +249 -0
  29. data/test/fixtures/files/fake/rails.png +0 -0
  30. data/test/fixtures/files/foo.txt +1 -0
  31. data/test/fixtures/files/rails.jpg +0 -0
  32. data/test/fixtures/files/rails.png +0 -0
  33. data/test/geometry_test.rb +114 -0
  34. data/test/processors/core_image_test.rb +50 -0
  35. data/test/processors/gd2_test.rb +44 -0
  36. data/test/processors/image_science_test.rb +47 -0
  37. data/test/processors/mini_magick_test.rb +116 -0
  38. data/test/processors/rmagick_test.rb +267 -0
  39. data/test/schema.rb +134 -0
  40. data/test/test_helper.rb +153 -0
  41. data/test/validation_test.rb +55 -0
  42. data/vendor/red_artisan/core_image/filters/color.rb +27 -0
  43. data/vendor/red_artisan/core_image/filters/effects.rb +31 -0
  44. data/vendor/red_artisan/core_image/filters/perspective.rb +25 -0
  45. data/vendor/red_artisan/core_image/filters/quality.rb +25 -0
  46. data/vendor/red_artisan/core_image/filters/scale.rb +47 -0
  47. data/vendor/red_artisan/core_image/filters/watermark.rb +32 -0
  48. data/vendor/red_artisan/core_image/processor.rb +123 -0
  49. metadata +103 -0
@@ -0,0 +1,126 @@
1
+ require 'fileutils'
2
+ require 'digest/sha2'
3
+
4
+ module Technoweenie # :nodoc:
5
+ module AttachmentFu # :nodoc:
6
+ module Backends
7
+ # Methods for file system backed attachments
8
+ module FileSystemBackend
9
+ def self.included(base) #:nodoc:
10
+ base.before_update :rename_file
11
+ end
12
+
13
+ # Gets the full path to the filename in this format:
14
+ #
15
+ # # This assumes a model name like MyModel
16
+ # # public/#{table_name} is the default filesystem path
17
+ # RAILS_ROOT/public/my_models/5/blah.jpg
18
+ #
19
+ # Overwrite this method in your model to customize the filename.
20
+ # The optional thumbnail argument will output the thumbnail's filename.
21
+ def full_filename(thumbnail = nil)
22
+ file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
23
+ File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
24
+ end
25
+
26
+ # Used as the base path that #public_filename strips off full_filename to create the public path
27
+ def base_path
28
+ @base_path ||= File.join(RAILS_ROOT, 'public')
29
+ end
30
+
31
+ # The attachment ID used in the full path of a file
32
+ def attachment_path_id
33
+ ((respond_to?(:parent_id) && parent_id) || id) || 0
34
+ end
35
+
36
+ # Partitions the given path into an array of path components.
37
+ #
38
+ # For example, given an <tt>*args</tt> of ["foo", "bar"], it will return
39
+ # <tt>["0000", "0001", "foo", "bar"]</tt> (assuming that that id returns 1).
40
+ #
41
+ # If the id is not an integer, then path partitioning will be performed by
42
+ # hashing the string value of the id with SHA-512, and splitting the result
43
+ # into 4 components. If the id a 128-bit UUID (as set by :uuid_primary_key => true)
44
+ # then it will be split into 2 components.
45
+ #
46
+ # To turn this off entirely, set :partition => false.
47
+ def partitioned_path(*args)
48
+ if respond_to?(:attachment_options) && attachment_options[:partition] == false
49
+ args
50
+ elsif attachment_options[:uuid_primary_key]
51
+ # Primary key is a 128-bit UUID in hex format. Split it into 2 components.
52
+ path_id = attachment_path_id.to_s
53
+ component1 = path_id[0..15] || "-"
54
+ component2 = path_id[16..-1] || "-"
55
+ [component1, component2] + args
56
+ else
57
+ path_id = attachment_path_id
58
+ if path_id.is_a?(Integer)
59
+ # Primary key is an integer. Split it after padding it with 0.
60
+ ("%08d" % path_id).scan(/..../) + args
61
+ else
62
+ # Primary key is a String. Hash it, then split it into 4 components.
63
+ hash = Digest::SHA512.hexdigest(path_id.to_s)
64
+ [hash[0..31], hash[32..63], hash[64..95], hash[96..127]] + args
65
+ end
66
+ end
67
+ end
68
+
69
+ # Gets the public path to the file
70
+ # The optional thumbnail argument will output the thumbnail's filename.
71
+ def public_filename(thumbnail = nil)
72
+ full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
73
+ end
74
+
75
+ def filename=(value)
76
+ @old_filename = full_filename unless filename.nil? || @old_filename
77
+ write_attribute :filename, sanitize_filename(value)
78
+ end
79
+
80
+ # Creates a temp file from the currently saved file.
81
+ def create_temp_file
82
+ copy_to_temp_file full_filename
83
+ end
84
+
85
+ protected
86
+ # Destroys the file. Called in the after_destroy callback
87
+ def destroy_file
88
+ FileUtils.rm full_filename
89
+ # remove directory also if it is now empty
90
+ Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
91
+ rescue
92
+ logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
93
+ logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
94
+ end
95
+
96
+ # Renames the given file before saving
97
+ def rename_file
98
+ return unless @old_filename && @old_filename != full_filename
99
+ if save_attachment? && File.exists?(@old_filename)
100
+ FileUtils.rm @old_filename
101
+ elsif File.exists?(@old_filename)
102
+ FileUtils.mv @old_filename, full_filename
103
+ end
104
+ @old_filename = nil
105
+ true
106
+ end
107
+
108
+ # Saves the file to the file system
109
+ def save_to_storage
110
+ if save_attachment?
111
+ # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
112
+ FileUtils.mkdir_p(File.dirname(full_filename))
113
+ FileUtils.cp(temp_path, full_filename)
114
+ FileUtils.chmod(attachment_options[:chmod] || 0644, full_filename)
115
+ end
116
+ @old_filename = nil
117
+ true
118
+ end
119
+
120
+ def current_data
121
+ File.file?(full_filename) ? File.read(full_filename) : nil
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -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_filename will be the CloudFront
170
+ # URL, not the S3 URL.
171
+ module S3Backend
172
+ class RequiredLibraryNotFoundError < StandardError; end
173
+ class ConfigFileNotFoundError < StandardError; end
174
+
175
+ def self.included(base) #:nodoc:
176
+ mattr_reader :bucket_name, :s3_config
177
+
178
+ begin
179
+ require 'aws/s3'
180
+ include AWS::S3
181
+ rescue LoadError
182
+ raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
183
+ end
184
+
185
+ begin
186
+ @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
187
+ @@s3_config = @@s3_config = YAML.load(ERB.new(File.read(@@s3_config_path)).result)[RAILS_ENV].symbolize_keys
188
+ #rescue
189
+ # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
190
+ end
191
+
192
+ bucket_key = base.attachment_options[:bucket_key]
193
+
194
+ if bucket_key and s3_config[bucket_key.to_sym]
195
+ eval_string = "def bucket_name()\n \"#{s3_config[bucket_key.to_sym]}\"\nend"
196
+ else
197
+ eval_string = "def bucket_name()\n \"#{s3_config[:bucket_name]}\"\nend"
198
+ end
199
+ base.class_eval(eval_string, __FILE__, __LINE__)
200
+
201
+ Base.establish_connection!(s3_config.slice(:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy))
202
+
203
+ # Bucket.create(@@bucket_name)
204
+
205
+ base.before_update :rename_file
206
+ end
207
+
208
+ def self.protocol
209
+ @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
210
+ end
211
+
212
+ def self.hostname
213
+ @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
214
+ end
215
+
216
+ def self.port_string
217
+ @port_string ||= (s3_config[:port].nil? || s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80)) ? '' : ":#{s3_config[:port]}"
218
+ end
219
+
220
+ def self.distribution_domain
221
+ @distribution_domain = s3_config[:distribution_domain]
222
+ end
223
+
224
+ module ClassMethods
225
+ def s3_protocol
226
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
227
+ end
228
+
229
+ def s3_hostname
230
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
231
+ end
232
+
233
+ def s3_port_string
234
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
235
+ end
236
+
237
+ def cloudfront_distribution_domain
238
+ Technoweenie::AttachmentFu::Backends::S3Backend.distribution_domain
239
+ end
240
+ end
241
+
242
+ # Overwrites the base filename writer in order to store the old filename
243
+ def filename=(value)
244
+ @old_filename = filename unless filename.nil? || @old_filename
245
+ write_attribute :filename, sanitize_filename(value)
246
+ end
247
+
248
+ # The attachment ID used in the full path of a file
249
+ def attachment_path_id
250
+ ((respond_to?(:parent_id) && parent_id) || id).to_s
251
+ end
252
+
253
+ # The pseudo hierarchy containing the file relative to the bucket name
254
+ # Example: <tt>:table_name/:id</tt>
255
+ def base_path
256
+ File.join(attachment_options[:path_prefix], attachment_path_id)
257
+ end
258
+
259
+ # The full path to the file relative to the bucket name
260
+ # Example: <tt>:table_name/:id/:filename</tt>
261
+ def full_filename(thumbnail = nil)
262
+ File.join(base_path, thumbnail_name_for(thumbnail))
263
+ end
264
+
265
+ # All public objects are accessible via a GET request to the S3 servers. You can generate a
266
+ # url for an object using the s3_url method.
267
+ #
268
+ # @photo.s3_url
269
+ #
270
+ # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
271
+ # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
272
+ # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
273
+ #
274
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
275
+ def s3_url(thumbnail = nil)
276
+ File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
277
+ end
278
+
279
+ # All public objects are accessible via a GET request to CloudFront. You can generate a
280
+ # url for an object using the cloudfront_url method.
281
+ #
282
+ # @photo.cloudfront_url
283
+ #
284
+ # The resulting url is in the form: <tt>http://:distribution_domain/:table_name/:id/:file</tt> using
285
+ # the <tt>:distribution_domain</tt> variable set in the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
286
+ #
287
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
288
+ def cloudfront_url(thumbnail = nil)
289
+ "http://" + cloudfront_distribution_domain + "/" + full_filename(thumbnail)
290
+ end
291
+
292
+ def public_filename(*args)
293
+ if attachment_options[:cloudfront]
294
+ cloudfront_url(args)
295
+ else
296
+ s3_url(args)
297
+ end
298
+ end
299
+
300
+ # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
301
+ # authenticated url for an object like this:
302
+ #
303
+ # @photo.authenticated_s3_url
304
+ #
305
+ # By default authenticated urls expire 5 minutes after they were generated.
306
+ #
307
+ # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
308
+ # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
309
+ #
310
+ # # Absolute expiration date (October 13th, 2025)
311
+ # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
312
+ #
313
+ # # Expiration in five hours from now
314
+ # @photo.authenticated_s3_url(:expires_in => 5.hours)
315
+ #
316
+ # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
317
+ # By default, the ssl settings for the current connection will be used:
318
+ #
319
+ # @photo.authenticated_s3_url(:use_ssl => true)
320
+ #
321
+ # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
322
+ #
323
+ # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
324
+ def authenticated_s3_url(*args)
325
+ options = args.extract_options!
326
+ options[:expires_in] = options[:expires_in].to_i if options[:expires_in]
327
+ thumbnail = args.shift
328
+ S3Object.url_for(full_filename(thumbnail), bucket_name, options)
329
+ end
330
+
331
+ def create_temp_file
332
+ write_to_temp_file current_data
333
+ end
334
+
335
+ def current_data
336
+ S3Object.value full_filename, bucket_name
337
+ end
338
+
339
+ def s3_protocol
340
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
341
+ end
342
+
343
+ def s3_hostname
344
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
345
+ end
346
+
347
+ def s3_port_string
348
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
349
+ end
350
+
351
+ def cloudfront_distribution_domain
352
+ Technoweenie::AttachmentFu::Backends::S3Backend.distribution_domain
353
+ end
354
+
355
+ protected
356
+ # Called in the after_destroy callback
357
+ def destroy_file
358
+ S3Object.delete full_filename, bucket_name
359
+ end
360
+
361
+ def rename_file
362
+ return unless @old_filename && @old_filename != filename
363
+
364
+ old_full_filename = File.join(base_path, @old_filename)
365
+
366
+ S3Object.rename(
367
+ old_full_filename,
368
+ full_filename,
369
+ bucket_name,
370
+ :access => attachment_options[:s3_access]
371
+ )
372
+
373
+ @old_filename = nil
374
+ true
375
+ end
376
+
377
+ def save_to_storage
378
+ if save_attachment?
379
+ S3Object.store(
380
+ full_filename,
381
+ (temp_path ? File.open(temp_path) : temp_data),
382
+ bucket_name,
383
+ :content_type => content_type,
384
+ :access => attachment_options[:s3_access]
385
+ )
386
+ end
387
+
388
+ @old_filename = nil
389
+ true
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,65 @@
1
+ require 'red_artisan/core_image/processor'
2
+
3
+ module Technoweenie # :nodoc:
4
+ module AttachmentFu # :nodoc:
5
+ module Processors
6
+ module CoreImageProcessor
7
+ def self.included(base)
8
+ base.send :extend, ClassMethods
9
+ base.alias_method_chain :process_attachment, :processing
10
+ end
11
+
12
+ module ClassMethods
13
+ def with_image(file, &block)
14
+ block.call OSX::CIImage.from(file)
15
+ end
16
+ end
17
+
18
+ protected
19
+ def process_attachment_with_processing
20
+ return unless process_attachment_without_processing
21
+ with_image do |img|
22
+ self.width = img.extent.size.width if respond_to?(:width)
23
+ self.height = img.extent.size.height if respond_to?(:height)
24
+ resize_image_or_thumbnail! img
25
+ callback_with_args :after_resize, img
26
+ end if image?
27
+ end
28
+
29
+ # Performs the actual resizing operation for a thumbnail
30
+ def resize_image(img, size)
31
+ processor = ::RedArtisan::CoreImage::Processor.new(img)
32
+ size = size.first if size.is_a?(Array) && size.length == 1
33
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
34
+ if size.is_a?(Fixnum)
35
+ processor.fit(size)
36
+ else
37
+ processor.resize(size[0], size[1])
38
+ end
39
+ else
40
+ new_size = [img.extent.size.width, img.extent.size.height] / size.to_s
41
+ processor.resize(new_size[0], new_size[1])
42
+ end
43
+
44
+ processor.render do |result|
45
+ self.width = result.extent.size.width if respond_to?(:width)
46
+ self.height = result.extent.size.height if respond_to?(:height)
47
+
48
+ # Get a new temp_path for the image before saving
49
+ out_file = random_tempfile_filename
50
+ temp_paths.unshift Tempfile.new(out_file, Technoweenie::AttachmentFu.tempfile_path).path
51
+ properties = nil
52
+ jpeg = out_file =~ /\.jpe?g\z/i
53
+ quality = attachment_options[:jpeg_quality]
54
+ quality = quality ? quality.to_i : -1
55
+ properties = { OSX::NSImageCompressionFactor => quality / 100.0 } if jpeg && quality.between?(0, 100)
56
+ result.save(self.temp_path, OSX::NSJPEGFileType, properties)
57
+ self.size = File.size(self.temp_path)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'gd2'
3
+ module Technoweenie # :nodoc:
4
+ module AttachmentFu # :nodoc:
5
+ module Processors
6
+ module Gd2Processor
7
+ def self.included(base)
8
+ base.send :extend, ClassMethods
9
+ base.alias_method_chain :process_attachment, :processing
10
+ end
11
+
12
+ module ClassMethods
13
+ # Yields a block containing a GD2 Image for the given binary data.
14
+ def with_image(file, &block)
15
+ im = GD2::Image.import(file)
16
+ block.call(im)
17
+ end
18
+ end
19
+
20
+ protected
21
+ def process_attachment_with_processing
22
+ return unless process_attachment_without_processing && image?
23
+ with_image do |img|
24
+ resize_image_or_thumbnail! img
25
+ self.width = img.width
26
+ self.height = img.height
27
+ callback_with_args :after_resize, img
28
+ end
29
+ end
30
+
31
+ # Performs the actual resizing operation for a thumbnail
32
+ def resize_image(img, size)
33
+ size = size.first if size.is_a?(Array) && size.length == 1
34
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
35
+ if size.is_a?(Fixnum)
36
+ # Borrowed from image science's #thumbnail method and adapted
37
+ # for this.
38
+ scale = size.to_f / (img.width > img.height ? img.width.to_f : img.height.to_f)
39
+ img.resize!((img.width * scale).round(1), (img.height * scale).round(1), false)
40
+ else
41
+ img.resize!(size.first, size.last, false)
42
+ end
43
+ else
44
+ w, h = [img.width, img.height] / size.to_s
45
+ img.resize!(w, h, false)
46
+ end
47
+ out_file = random_tempfile_filename
48
+ temp_paths.unshift out_file
49
+ jpeg = out_file =~ /\.jpe?g\z/i
50
+ quality = attachment_options[:jpeg_quality]
51
+ quality = quality ? quality.to_i : -1
52
+ self.size = if jpeg && quality.between?(0, 100)
53
+ img.export(self.temp_path, :quality => quality)
54
+ else
55
+ img.export(self.temp_path)
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
62
+ end