ncri_attachment_fu 0.1.8

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 (28) hide show
  1. data/CHANGELOG +35 -0
  2. data/LICENSE +20 -0
  3. data/README +193 -0
  4. data/amazon_s3.yml.tpl +17 -0
  5. data/lib/attachment_fu.rb +17 -0
  6. data/lib/geometry.rb +93 -0
  7. data/lib/technoweenie/attachment_fu.rb +533 -0
  8. data/lib/technoweenie/attachment_fu/backends.rb +4 -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.rb +1 -0
  14. data/lib/technoweenie/attachment_fu/processors/core_image_processor.rb +59 -0
  15. data/lib/technoweenie/attachment_fu/processors/gd2_processor.rb +54 -0
  16. data/lib/technoweenie/attachment_fu/processors/image_science_processor.rb +61 -0
  17. data/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb +132 -0
  18. data/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb +57 -0
  19. data/rackspace_cloudfiles.yml.tpl +14 -0
  20. data/rails/install.rb +7 -0
  21. data/vendor/red_artisan/core_image/filters/color.rb +27 -0
  22. data/vendor/red_artisan/core_image/filters/effects.rb +31 -0
  23. data/vendor/red_artisan/core_image/filters/perspective.rb +25 -0
  24. data/vendor/red_artisan/core_image/filters/quality.rb +25 -0
  25. data/vendor/red_artisan/core_image/filters/scale.rb +47 -0
  26. data/vendor/red_artisan/core_image/filters/watermark.rb +32 -0
  27. data/vendor/red_artisan/core_image/processor.rb +123 -0
  28. metadata +75 -0
@@ -0,0 +1,4 @@
1
+ require 'technoweenie/attachment_fu/backends/cloud_file_backend'
2
+ require 'technoweenie/attachment_fu/backends/db_file_backend'
3
+ require 'technoweenie/attachment_fu/backends/file_system_backend'
4
+ require 'technoweenie/attachment_fu/backends/s3_backend'
@@ -0,0 +1,211 @@
1
+ module Technoweenie # :nodoc:
2
+ module AttachmentFu # :nodoc:
3
+ module Backends
4
+ # = CloudFiles Storage Backend
5
+ #
6
+ # Enables use of {Rackspace Cloud Files}[http://www.mosso.com/cloudfiles.jsp] as a storage mechanism
7
+ #
8
+ # Based heavily on the Amazon S3 backend.
9
+ #
10
+ # == Requirements
11
+ #
12
+ # Requires the {Cloud Files Gem}[http://www.mosso.com/cloudfiles.jsp] by Rackspace
13
+ #
14
+ # == Configuration
15
+ #
16
+ # Configuration is done via <tt>Rails.root.to_s/config/rackspace_cloudfiles.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
17
+ # The minimum connection options that you must specify are a container name, your Mosso login name and your Mosso API key.
18
+ # You can sign up for Cloud Files and get access keys by visiting https://www.mosso.com/buy.htm
19
+ #
20
+ # Example configuration (Rails.root.to_s/config/rackspace_cloudfiles.yml)
21
+ #
22
+ # development:
23
+ # container_name: appname_development
24
+ # username: <your key>
25
+ # api_key: <your key>
26
+ #
27
+ # test:
28
+ # container_name: appname_test
29
+ # username: <your key>
30
+ # api_key: <your key>
31
+ #
32
+ # production:
33
+ # container_name: appname
34
+ # username: <your key>
35
+ # apik_key: <your key>
36
+ #
37
+ # You can change the location of the config path by passing a full path to the :cloudfiles_config_path option.
38
+ #
39
+ # has_attachment :storage => :cloud_files, :cloudfiles_config_path => (Rails.root.to_s + '/config/mosso.yml')
40
+ #
41
+ # === Required configuration parameters
42
+ #
43
+ # * <tt>:username</tt> - The username for your Rackspace Cloud (Mosso) account. Provided by Rackspace.
44
+ # * <tt>:secret_access_key</tt> - The api key for your Rackspace Cloud account. Provided by Rackspace.
45
+ # * <tt>:container_name</tt> - The name of a container in your Cloud Files account.
46
+ #
47
+ # If any of these required arguments is missing, a AuthenticationException will be raised from CloudFiles::Connection.
48
+ #
49
+ # == Usage
50
+ #
51
+ # To specify Cloud Files as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:cloud_files/tt>.
52
+ #
53
+ # class Photo < ActiveRecord::Base
54
+ # has_attachment :storage => :cloud_files
55
+ # end
56
+ #
57
+ # === Customizing the path
58
+ #
59
+ # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
60
+ # in Cloud Files object names (and urls) that look like: http://:server/:container_name/:table_name/:id/:filename with :table_name
61
+ # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
62
+ # option:
63
+ #
64
+ # class Photo < ActiveRecord::Base
65
+ # has_attachment :storage => :cloud_files, :path_prefix => 'my/custom/path'
66
+ # end
67
+ #
68
+ # Which would result in public URLs like <tt>http(s)://:server/:container_name/my/custom/path/:id/:filename.</tt>
69
+ #
70
+ # === Permissions
71
+ #
72
+ # File permisisons are determined by the permissions of the container. At present, the options are public (and distributed
73
+ # by the Limelight CDN), and private (only available to your login)
74
+ #
75
+ # === Other options
76
+ #
77
+ # Of course, all the usual configuration options apply, such as content_type and thumbnails:
78
+ #
79
+ # class Photo < ActiveRecord::Base
80
+ # has_attachment :storage => :cloud_files, :content_type => ['application/pdf', :image], :resize_to => 'x50'
81
+ # has_attachment :storage => :cloud_files, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
82
+ # end
83
+ #
84
+ # === Accessing Cloud Files URLs
85
+ #
86
+ # You can get an object's public URL using the cloudfiles_url accessor. For example, assuming that for your postcard app
87
+ # you had a container name like 'postcard_world_development', and an attachment model called Photo:
88
+ #
89
+ # @postcard.cloudfiles_url # => http://cdn.cloudfiles.mosso.com/c45182/uploaded_files/20/london.jpg
90
+ #
91
+ # The resulting url is in the form: http://:server/:container_name/:table_name/:id/:file.
92
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
93
+ #
94
+ # Additionally, you can get an object's base path relative to the container root using
95
+ # <tt>base_path</tt>:
96
+ #
97
+ # @photo.file_base_path # => uploaded_files/20
98
+ #
99
+ # And the full path (including the filename) using <tt>full_filename</tt>:
100
+ #
101
+ # @photo.full_filename # => uploaded_files/20/london.jpg
102
+ #
103
+ # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the container name as part of the path.
104
+ # You can retrieve the container name using the <tt>container_name</tt> method.
105
+ module CloudFileBackend
106
+ class RequiredLibraryNotFoundError < StandardError; end
107
+ class ConfigFileNotFoundError < StandardError; end
108
+
109
+ def self.included(base) #:nodoc:
110
+ mattr_reader :container_name, :cloudfiles_config
111
+
112
+ begin
113
+ require 'cloudfiles'
114
+ rescue LoadError
115
+ raise RequiredLibraryNotFoundError.new('CloudFiles could not be loaded')
116
+ end
117
+
118
+ begin
119
+ @@cloudfiles_config_path = base.attachment_options[:cloudfiles_config_path] || (Rails.root.to_s + '/config/rackspace_cloudfiles.yml')
120
+ @@cloudfiles_config = @@cloudfiles_config = YAML.load(ERB.new(File.read(@@cloudfiles_config_path)).result)[Rails.env].symbolize_keys
121
+ rescue
122
+ #raise ConfigFileNotFoundError.new('File %s not found' % @@cloudfiles_config_path)
123
+ end
124
+
125
+ @@container_name = @@cloudfiles_config[:container_name]
126
+ @@cf = CloudFiles::Connection.new(@@cloudfiles_config[:username], @@cloudfiles_config[:api_key])
127
+ @@container = @@cf.container(@@container_name)
128
+
129
+ base.before_update :rename_file
130
+ end
131
+
132
+ # Overwrites the base filename writer in order to store the old filename
133
+ def filename=(value)
134
+ @old_filename = filename unless filename.nil? || @old_filename
135
+ write_attribute :filename, sanitize_filename(value)
136
+ end
137
+
138
+ # The attachment ID used in the full path of a file
139
+ def attachment_path_id
140
+ ((respond_to?(:parent_id) && parent_id) || id).to_s
141
+ end
142
+
143
+ # The pseudo hierarchy containing the file relative to the container name
144
+ # Example: <tt>:table_name/:id</tt>
145
+ def base_path
146
+ File.join(attachment_options[:path_prefix], attachment_path_id)
147
+ end
148
+
149
+ # The full path to the file relative to the container name
150
+ # Example: <tt>:table_name/:id/:filename</tt>
151
+ def full_filename(thumbnail = nil)
152
+ File.join(base_path, thumbnail_name_for(thumbnail))
153
+ end
154
+
155
+ # All public objects are accessible via a GET request to the Cloud Files servers. You can generate a
156
+ # url for an object using the cloudfiles_url method.
157
+ #
158
+ # @photo.cloudfiles_url
159
+ #
160
+ # The resulting url is in the CDN URL for the object
161
+ #
162
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
163
+ #
164
+ # If you are trying to get the URL for a nonpublic container, nil will be returned.
165
+ def cloudfiles_url(thumbnail = nil)
166
+ if @@container.public?
167
+ File.join(@@container.cdn_url, full_filename(thumbnail))
168
+ else
169
+ nil
170
+ end
171
+ end
172
+ alias :public_filename :cloudfiles_url
173
+
174
+ def create_temp_file
175
+ write_to_temp_file current_data
176
+ end
177
+
178
+ def current_data
179
+ @@container.get_object(full_filename).data
180
+ end
181
+
182
+ protected
183
+ # Called in the after_destroy callback
184
+ def destroy_file
185
+ @@container.delete_object(full_filename)
186
+ end
187
+
188
+ def rename_file
189
+ # Cloud Files doesn't rename right now, so we'll just nuke.
190
+ return unless @old_filename && @old_filename != filename
191
+
192
+ old_full_filename = File.join(base_path, @old_filename)
193
+ @@container.delete_object(old_full_filename)
194
+
195
+ @old_filename = nil
196
+ true
197
+ end
198
+
199
+ def save_to_storage
200
+ if save_attachment?
201
+ @object = @@container.create_object(full_filename)
202
+ @object.write((temp_path ? File.open(temp_path) : temp_data))
203
+ end
204
+
205
+ @old_filename = nil
206
+ true
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,39 @@
1
+ module Technoweenie # :nodoc:
2
+ module AttachmentFu # :nodoc:
3
+ module Backends
4
+ # Methods for DB backed attachments
5
+ module DbFileBackend
6
+ def self.included(base) #:nodoc:
7
+ Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile)
8
+ base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id'
9
+ end
10
+
11
+ # Creates a temp file with the current db data.
12
+ def create_temp_file
13
+ write_to_temp_file current_data
14
+ end
15
+
16
+ # Gets the current data from the database
17
+ def current_data
18
+ db_file.data
19
+ end
20
+
21
+ protected
22
+ # Destroys the file. Called in the after_destroy callback
23
+ def destroy_file
24
+ db_file.destroy if db_file
25
+ end
26
+
27
+ # Saves the data to the DbFile model
28
+ def save_to_storage
29
+ if save_attachment?
30
+ (db_file || build_db_file).data = temp_data
31
+ db_file.save!
32
+ self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
33
+ end
34
+ true
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -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.to_s/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.to_s, 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.to_s, '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.to_s/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.to_s/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.to_s + '/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.to_s + '/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.to_s/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.to_s/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