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,19 @@
1
+ module Technoweenie # :nodoc:
2
+ module AttachmentFu # :nodoc:
3
+ module Backends
4
+ class BackendDelegator < Delegator
5
+ attr_accessor :attachment_options
6
+
7
+ def initialize(obj, opts)
8
+ @obj = obj
9
+ @attachment_options = opts
10
+ end
11
+
12
+ def __getobj__
13
+ @obj
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,245 @@
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/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/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 + '/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
+ class CloudFileBackend < BackendDelegator
106
+ class RequiredLibraryNotFoundError < StandardError; end
107
+ class ConfigFileNotFoundError < StandardError; end
108
+
109
+ cattr_reader :cloudfiles_config, :container_name
110
+ def self.included_in_base(base) #:nodoc:
111
+
112
+ begin
113
+ require 'cloudfiles'
114
+ rescue LoadError
115
+ raise RequiredLibraryNotFoundError.new('CloudFiles could not be loaded')
116
+ end
117
+
118
+ opts = base.attachment_options
119
+ if opts[:cloudfiles_options]
120
+ @@cloudfiles_config = opts[:cloudfiles_options]
121
+ elsif opts[:cloudfiles_username] && opts[:cloudfiles_api_key] && opts[:cloudfiles_container_name]
122
+ @@cloudfiles_config = {:container_name => opts[:cloudfiles_container_name],
123
+ :username => opts[:cloudfiles_username],
124
+ :api_key => opts[:cloudfiles_api_key]}
125
+ else
126
+ @@cloudfiles_config_path = base.attachment_options[:cloudfiles_config_path] || Rails.root.join('config/rackspace_cloudfiles.yml').to_s
127
+ @@cloudfiles_config = @@cloudfiles_config = YAML.load(ERB.new(File.read(@@cloudfiles_config_path)).result)[ENV['RAILS_ENV']].symbolize_keys
128
+ base.attachment_options[:cloudfiles_container_name] = @@cloudfiles_config[:container_name]
129
+ end
130
+ end
131
+
132
+ def container_name
133
+ return @obj.database_container_name if @obj.respond_to?(:database_container_name) && @obj.database_container_name
134
+ @@cloudfiles_config[:container_name]
135
+ end
136
+
137
+ def self.connection
138
+ @@cf ||= CloudFiles::Connection.new(@@cloudfiles_config)
139
+ end
140
+
141
+ def container
142
+ self.class.connection.container(container_name)
143
+ end
144
+
145
+ def cloudfiles_authtoken
146
+ self.class.connection.authtoken
147
+ end
148
+
149
+ def cloudfiles_storage_url
150
+ cx = self.class.connection
151
+ cx.storagescheme + "://" + (cx.storageport ? "" : ":#{cx.storageport}") + cx.storagehost + cx.storagepath
152
+ end
153
+
154
+ # Overwrites the base filename writer in order to store the old filename
155
+ def notify_rename
156
+ @old_filename = filename unless filename.nil? || @old_filename
157
+ end
158
+
159
+ # The attachment ID used in the full path of a file
160
+ def attachment_path_id
161
+ ((respond_to?(:parent_id) && parent_id) || @obj.id).to_s
162
+ end
163
+
164
+ # The pseudo hierarchy containing the file relative to the container name
165
+ # Example: <tt>:table_name/:id</tt>
166
+ def base_path
167
+ File.join(attachment_options[:path_prefix], attachment_path_id)
168
+ end
169
+
170
+ # The full path to the file relative to the container name
171
+ # Example: <tt>:table_name/:id/:filename</tt>
172
+ def full_filename(thumbnail = nil)
173
+ File.join(base_path, thumbnail_name_for(thumbnail))
174
+ end
175
+
176
+ # All public objects are accessible via a GET request to the Cloud Files servers. You can generate a
177
+ # url for an object using the cloudfiles_url method.
178
+ #
179
+ # @photo.cloudfiles_url
180
+ #
181
+ # The resulting url is in the CDN URL for the object
182
+ #
183
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
184
+ #
185
+ # If you are trying to get the URL for a nonpublic container, nil will be returned.
186
+ def cloudfiles_url(thumbnail = nil)
187
+ if container.public?
188
+ File.join(container.cdn_url, full_filename(thumbnail))
189
+ else
190
+ nil
191
+ end
192
+ end
193
+ alias :public_url :cloudfiles_url
194
+
195
+ def create_temp_file
196
+ write_to_temp_file current_data
197
+ end
198
+
199
+ def current_data
200
+ container.get_object(full_filename).data
201
+ end
202
+
203
+ # Called in the after_destroy callback
204
+ def destroy_file
205
+ retried = false
206
+ begin
207
+ container.delete_object(full_filename)
208
+ rescue CloudFiles::Exception::NoSuchObject => e
209
+ rescue CloudFiles::Exception::InvalidResponse => e
210
+ if retried
211
+ raise e
212
+ else
213
+ retried = true
214
+ retry
215
+ end
216
+ end
217
+ end
218
+
219
+ def rename_file
220
+ # Cloud Files doesn't rename right now, so we'll just nuke.
221
+ return unless @old_filename && @old_filename != filename
222
+
223
+ old_full_filename = File.join(base_path, @old_filename)
224
+ begin
225
+ container.delete_object(old_full_filename)
226
+ rescue CloudFiles::Exception::NoSuchObject => e
227
+ end
228
+
229
+ @old_filename = nil
230
+ true
231
+ end
232
+
233
+ def save_to_storage
234
+ if save_attachment?
235
+ @object = container.create_object(full_filename)
236
+ @object.write((temp_path ? File.open(temp_path) : temp_data))
237
+ end
238
+
239
+ @old_filename = nil
240
+ true
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,36 @@
1
+ module Technoweenie # :nodoc:
2
+ module AttachmentFu # :nodoc:
3
+ module Backends
4
+ # Methods for DB backed attachments
5
+ class DbFileBackend < BackendDelegator
6
+ def self.included_in_base(base)
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
+ def rename_file ; end
12
+
13
+ # Gets the current data from the database
14
+ def current_data
15
+ db_file && db_file.data
16
+ end
17
+
18
+ # Destroys the file. Called in the after_destroy callback
19
+ def destroy_file
20
+ db_file.destroy if db_file
21
+ end
22
+
23
+ # Saves the data to the DbFile model
24
+ def save_to_storage
25
+ if save_attachment?
26
+ (db_file || build_db_file).data = temp_data
27
+ db_file.save!
28
+ @obj.db_file_id = db_file.id
29
+ @obj.class.where(id: @obj.id).update_all db_file_id: db_file.id
30
+ end
31
+ true
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,125 @@
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
+ class FileSystemBackend < BackendDelegator
9
+ def self.included_in_base(base)
10
+ end
11
+
12
+ # Gets the full path to the filename in this format:
13
+ #
14
+ # # This assumes a model name like MyModel
15
+ # # public/#{table_name} is the default filesystem path
16
+ # RAILS_ROOT/public/my_models/5/blah.jpg
17
+ #
18
+ # Overwrite this method in your model to customize the filename.
19
+ # The optional thumbnail argument will output the thumbnail's filename.
20
+ def full_filename(thumbnail = nil)
21
+ file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
22
+ Rails.root.join(file_system_path, *partitioned_path(thumbnail_name_for(thumbnail))).to_s
23
+ end
24
+
25
+ # Used as the base path that #public_filename strips off full_filename to create the public path
26
+ def base_path
27
+ @base_path ||= Rails.root.join('public').to_s
28
+ end
29
+
30
+ # The attachment ID used in the full path of a file
31
+ def attachment_path_id
32
+ ((respond_to?(:parent_id) && parent_id) || read_attribute(:id)) || 0
33
+ end
34
+
35
+ # Partitions the given path into an array of path components.
36
+ #
37
+ # For example, given an <tt>*args</tt> of ["foo", "bar"], it will return
38
+ # <tt>["0000", "0001", "foo", "bar"]</tt> (assuming that that id returns 1).
39
+ #
40
+ # If the id is not an integer, then path partitioning will be performed by
41
+ # hashing the string value of the id with SHA-512, and splitting the result
42
+ # into 4 components. If the id a 128-bit UUID (as set by :uuid_primary_key => true)
43
+ # then it will be split into 2 components.
44
+ #
45
+ # To turn this off entirely, set :partition => false.
46
+ def partitioned_path(*args)
47
+ if respond_to?(:attachment_options) && attachment_options[:partition] == false
48
+ args
49
+ elsif attachment_options[:uuid_primary_key]
50
+ # Primary key is a 128-bit UUID in hex format. Split it into 2 components.
51
+ path_id = attachment_path_id.to_s
52
+ component1 = path_id[0..15] || "-"
53
+ component2 = path_id[16..-1] || "-"
54
+ [component1, component2] + args
55
+ else
56
+ path_id = attachment_path_id
57
+ if path_id.is_a?(Integer)
58
+ partitioned_path_for_fixnum(path_id, args)
59
+ else
60
+ # Primary key is a String. Hash it, then split it into 4 components.
61
+ hash = Digest::SHA512.hexdigest(path_id.to_s)
62
+ [hash[0..31], hash[32..63], hash[64..95], hash[96..127]] + args
63
+ end
64
+ end
65
+ end
66
+
67
+ def partitioned_path_for_fixnum(path_id, args)
68
+ if path_id <= 9999_9999
69
+ ("%08d" % path_id).scan(/..../) + args
70
+ else
71
+ ("%012d" % path_id).scan(/..../) + args
72
+ end
73
+ end
74
+
75
+ # Gets the public path to the file
76
+ # The optional thumbnail argument will output the thumbnail's filename.
77
+ def public_filename(thumbnail = nil)
78
+ full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
79
+ end
80
+
81
+ def notify_rename
82
+ @old_filename = full_filename unless filename.nil? || @old_filename
83
+ end
84
+
85
+ # Destroys the file. Called in the after_destroy callback
86
+ def destroy_file
87
+ FileUtils.rm full_filename
88
+ # remove directory also if it is now empty
89
+ Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
90
+ rescue
91
+ logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
92
+ logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
93
+ end
94
+
95
+ # Renames the given file before saving
96
+ def rename_file
97
+ return unless @old_filename && @old_filename != full_filename
98
+ if save_attachment? && File.exists?(@old_filename)
99
+ FileUtils.rm @old_filename
100
+ elsif File.exists?(@old_filename)
101
+ FileUtils.mv @old_filename, full_filename
102
+ end
103
+ @old_filename = nil
104
+ true
105
+ end
106
+
107
+ # Saves the file to the file system
108
+ def save_to_storage
109
+ if save_attachment?
110
+ # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
111
+ FileUtils.mkdir_p(File.dirname(full_filename))
112
+ FileUtils.cp(temp_path, full_filename)
113
+ FileUtils.chmod(attachment_options[:chmod] || 0644, full_filename)
114
+ end
115
+ @old_filename = nil
116
+ true
117
+ end
118
+
119
+ def current_data
120
+ File.file?(full_filename) ? File.read(full_filename) : nil
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,99 @@
1
+ module Technoweenie # :nodoc:
2
+ module AttachmentFu # :nodoc:
3
+ module Backends
4
+ class MogileFSBackend < BackendDelegator
5
+ class ConfigFileNotFoundError < StandardError; end
6
+
7
+ class RequiredLibraryNotFoundError < StandardError; end
8
+ attr_reader :mogile_domain_name
9
+ def initialize(obj, opts)
10
+ @domain_name = opts[:mogile_domain_name] || @@mogile_config[:domain_name]
11
+ super(obj, opts)
12
+ end
13
+
14
+ def self.included_in_base(base) #:nodoc:
15
+ begin
16
+ require 'mogilefs'
17
+ rescue LoadError
18
+ raise RequiredLibraryNotFoundError.new('mogilefs could not be loaded')
19
+ end
20
+
21
+ mogile_config = nil
22
+ if base.attachment_options[:mogile_hosts] && base.attachment_options[:mogile_domain_name]
23
+ mogile_config = base.attachment_options.inject({}) do |memo, arr|
24
+ k, v = arr
25
+ memo[k.to_s.gsub(/^mogile_/, '').to_sym] = v if k.to_s =~ /^mogile_/
26
+ memo
27
+ end
28
+ else
29
+ mogile_config_path = base.attachment_options[:mogile_config_path] || Rails.root.join('config/mogilefs.yml').to_s
30
+ mogile_config = YAML.load(ERB.new(File.read(mogile_config_path)).result)[RAILS_ENV].symbolize_keys
31
+ end
32
+
33
+ @@mogile_config = mogile_config
34
+ @@mogile = MogileFS::MogileFS.new(:domain => @@mogile_config[:domain_name], :hosts => @@mogile_config[:hosts])
35
+ end
36
+
37
+ # called by the ActiveRecord class from filename=
38
+ def notify_rename
39
+ @old_filename = filename unless filename.nil? || @old_filename
40
+ end
41
+
42
+ # The attachment ID used in the full path of a file
43
+ def attachment_path_id
44
+ ((respond_to?(:parent_id) && parent_id) || @obj.id).to_s
45
+ end
46
+
47
+ # The pseudo hierarchy containing the file relative to the bucket name
48
+ # Example: <tt>:table_name/:id</tt>
49
+ def base_path
50
+ File.join(attachment_options[:path_prefix], attachment_path_id)
51
+ end
52
+
53
+ def full_filename(thumbnail = nil)
54
+ File.join(base_path, thumbnail_name_for(thumbnail))
55
+ end
56
+
57
+ def current_data
58
+ @@mogile.get_file_data(full_filename)
59
+ end
60
+
61
+ # Called in the after_destroy callback
62
+ def destroy_file
63
+ @@mogile.delete(full_filename)
64
+ end
65
+
66
+ def rename_file
67
+ return unless @old_filename && @old_filename != filename
68
+
69
+ old_full_filename = File.join(base_path, @old_filename)
70
+
71
+ begin
72
+ @@mogile.rename(old_full_filename, full_filename)
73
+ rescue MogileFS::Backend::KeyExistsError
74
+ # this is hacky. It's at first blush actually a limitation of the mogilefs-client gem,
75
+ # which always tries to create a new key instead of exposing "set" semantics.
76
+ @@mogile.delete(full_filename)
77
+ retry
78
+ end
79
+
80
+ @old_filename = nil
81
+ true
82
+ end
83
+
84
+ def save_to_storage
85
+ if save_attachment?
86
+ if temp_path
87
+ @@mogile.store_file(full_filename, @@mogile_config[:mogile_storage_class], temp_path)
88
+ else
89
+ @@mogile.store_content(full_filename, @@mogile_config[:mogile_storage_class], temp_data)
90
+ end
91
+ end
92
+
93
+ @old_filename = nil
94
+ true
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end