attachment_zen 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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