attachmerb_fu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/LICENSE +22 -0
  2. data/README +166 -0
  3. data/Rakefile +35 -0
  4. data/TODO +5 -0
  5. data/lib/amazon_s3.yml.tpl +14 -0
  6. data/lib/attachment_fu.rb +431 -0
  7. data/lib/attachmerb_fu.rb +446 -0
  8. data/lib/attachmerb_fu/backends/db_file_backend.rb +37 -0
  9. data/lib/attachmerb_fu/backends/file_system_backend.rb +95 -0
  10. data/lib/attachmerb_fu/backends/s3_backend.rb +307 -0
  11. data/lib/attachmerb_fu/merbtasks.rb +6 -0
  12. data/lib/attachmerb_fu/processors/image_science_processor.rb +60 -0
  13. data/lib/attachmerb_fu/processors/mini_magick_processor.rb +54 -0
  14. data/lib/attachmerb_fu/processors/rmagick_processor.rb +51 -0
  15. data/lib/geometry.rb +93 -0
  16. data/lib/tempfile_ext.rb +9 -0
  17. data/lib/test/amazon_s3.yml +6 -0
  18. data/lib/test/backends/db_file_test.rb +16 -0
  19. data/lib/test/backends/file_system_test.rb +80 -0
  20. data/lib/test/backends/remote/s3_test.rb +103 -0
  21. data/lib/test/base_attachment_tests.rb +57 -0
  22. data/lib/test/basic_test.rb +64 -0
  23. data/lib/test/database.yml +18 -0
  24. data/lib/test/extra_attachment_test.rb +57 -0
  25. data/lib/test/fixtures/attachment.rb +127 -0
  26. data/lib/test/fixtures/files/fake/rails.png +0 -0
  27. data/lib/test/fixtures/files/foo.txt +1 -0
  28. data/lib/test/fixtures/files/rails.png +0 -0
  29. data/lib/test/geometry_test.rb +101 -0
  30. data/lib/test/processors/image_science_test.rb +31 -0
  31. data/lib/test/processors/mini_magick_test.rb +31 -0
  32. data/lib/test/processors/rmagick_test.rb +241 -0
  33. data/lib/test/schema.rb +86 -0
  34. data/lib/test/test_helper.rb +142 -0
  35. data/lib/test/validation_test.rb +55 -0
  36. metadata +107 -0
@@ -0,0 +1,95 @@
1
+ require 'ftools'
2
+ module AttachmerbFu # :nodoc:
3
+ module Backends
4
+ # Methods for file system backed attachments
5
+ module FileSystemBackend
6
+ def self.included(base) #:nodoc:
7
+ base.before_update :rename_file
8
+ end
9
+
10
+ # Gets the full path to the filename in this format:
11
+ #
12
+ # # This assumes a model name like MyModel
13
+ # # public/#{table_name} is the default filesystem path
14
+ # Merb.root/public/my_models/5/blah.jpg
15
+ #
16
+ # Overwrite this method in your model to customize the filename.
17
+ # The optional thumbnail argument will output the thumbnail's filename.
18
+ def full_filename(thumbnail = nil)
19
+ file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
20
+ File.join(Merb.root, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
21
+ end
22
+
23
+ # Used as the base path that #public_filename strips off full_filename to create the public path
24
+ def base_path
25
+ @base_path ||= File.join(Merb.root, 'public')
26
+ end
27
+
28
+ # The attachment ID used in the full path of a file
29
+ def attachment_path_id
30
+ ((respond_to?(:parent_id) && parent_id) || id).to_i
31
+ end
32
+
33
+ # overrwrite this to do your own app-specific partitioning.
34
+ # you can thank Jamis Buck for this: http://www.37signals.com/svn/archives2/id_partitioning.php
35
+ def partitioned_path(*args)
36
+ ("%08d" % attachment_path_id).scan(/..../) + args
37
+ end
38
+
39
+ # Gets the public path to the file
40
+ # The optional thumbnail argument will output the thumbnail's filename.
41
+ def public_filename(thumbnail = nil)
42
+ full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
43
+ end
44
+
45
+ def filename=(value)
46
+ @old_filename = full_filename unless filename.nil? || @old_filename
47
+ write_attribute :filename, sanitize_filename(value)
48
+ end
49
+
50
+ # Creates a temp file from the currently saved file.
51
+ def create_temp_file
52
+ copy_to_temp_file full_filename
53
+ end
54
+
55
+ protected
56
+ # Destroys the file. Called in the after_destroy callback
57
+ def destroy_file
58
+ FileUtils.rm full_filename
59
+ # remove directory also if it is now empty
60
+ Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
61
+ rescue
62
+ logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
63
+ logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
64
+ end
65
+
66
+ # Renames the given file before saving
67
+ def rename_file
68
+ return unless @old_filename && @old_filename != full_filename
69
+ if save_attachment? && File.exists?(@old_filename)
70
+ FileUtils.rm @old_filename
71
+ elsif File.exists?(@old_filename)
72
+ FileUtils.mv @old_filename, full_filename
73
+ end
74
+ @old_filename = nil
75
+ true
76
+ end
77
+
78
+ # Saves the file to the file system
79
+ def save_to_storage
80
+ if save_attachment?
81
+ # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
82
+ FileUtils.mkdir_p(File.dirname(full_filename))
83
+ File.cp(temp_path, full_filename)
84
+ File.chmod(attachment_options[:chmod] || 0644, full_filename)
85
+ end
86
+ @old_filename = nil
87
+ true
88
+ end
89
+
90
+ def current_data
91
+ File.file?(full_filename) ? File.read(full_filename) : nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,307 @@
1
+ module AttachmerbFu # :nodoc:
2
+ module Backends
3
+ # = AWS::S3 Storage Backend
4
+ #
5
+ # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism
6
+ #
7
+ # == Requirements
8
+ #
9
+ # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either
10
+ # as a gem or a as a Rails plugin.
11
+ #
12
+ # == Configuration
13
+ #
14
+ # Configuration is done via <tt>RAILS_ROOT/config/amazon_s3.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
15
+ # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key.
16
+ # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon.
17
+ # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.
18
+ #
19
+ # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
20
+ #
21
+ # development:
22
+ # bucket_name: appname_development
23
+ # access_key_id: <your key>
24
+ # secret_access_key: <your key>
25
+ #
26
+ # test:
27
+ # bucket_name: appname_test
28
+ # access_key_id: <your key>
29
+ # secret_access_key: <your key>
30
+ #
31
+ # production:
32
+ # bucket_name: appname
33
+ # access_key_id: <your key>
34
+ # secret_access_key: <your key>
35
+ #
36
+ # You can change the location of the config path by passing a full path to the :s3_config_path option.
37
+ #
38
+ # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
39
+ #
40
+ # === Required configuration parameters
41
+ #
42
+ # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
43
+ # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
44
+ # * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
45
+ #
46
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
47
+ #
48
+ # == About bucket names
49
+ #
50
+ # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
51
+ # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
52
+ # implementation to the development, test, and production environments.
53
+ #
54
+ # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
55
+ #
56
+ # === Optional configuration parameters
57
+ #
58
+ # * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
59
+ # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
60
+ # * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
61
+ #
62
+ # == Usage
63
+ #
64
+ # To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
65
+ #
66
+ # class Photo < ActiveRecord::Base
67
+ # has_attachment :storage => :s3
68
+ # end
69
+ #
70
+ # === Customizing the path
71
+ #
72
+ # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
73
+ # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
74
+ # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
75
+ # option:
76
+ #
77
+ # class Photo < ActiveRecord::Base
78
+ # has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
79
+ # end
80
+ #
81
+ # Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
82
+ #
83
+ # === Permissions
84
+ #
85
+ # By default, files are stored on S3 with public access permissions. You can customize this using
86
+ # the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are
87
+ # <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
88
+ #
89
+ # === Other options
90
+ #
91
+ # Of course, all the usual configuration options apply, such as content_type and thumbnails:
92
+ #
93
+ # class Photo < ActiveRecord::Base
94
+ # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
95
+ # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
96
+ # end
97
+ #
98
+ # === Accessing S3 URLs
99
+ #
100
+ # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
101
+ # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
102
+ #
103
+ # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
104
+ #
105
+ # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
106
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
107
+ #
108
+ # Additionally, you can get an object's base path relative to the bucket root using
109
+ # <tt>base_path</tt>:
110
+ #
111
+ # @photo.file_base_path # => photos/1
112
+ #
113
+ # And the full path (including the filename) using <tt>full_filename</tt>:
114
+ #
115
+ # @photo.full_filename # => photos/
116
+ #
117
+ # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
118
+ # You can retrieve the bucket name using the <tt>bucket_name</tt> method.
119
+ module S3Backend
120
+ class RequiredLibraryNotFoundError < StandardError; end
121
+ class ConfigFileNotFoundError < StandardError; end
122
+
123
+ def self.included(base) #:nodoc:
124
+ mattr_reader :bucket_name, :s3_config
125
+
126
+ begin
127
+ require 'aws/s3'
128
+ include AWS::S3
129
+ rescue LoadError
130
+ raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
131
+ end
132
+
133
+ begin
134
+ @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
135
+ @@s3_config = @@s3_config = YAML.load_file(@@s3_config_path)[RAILS_ENV].symbolize_keys
136
+ #rescue
137
+ # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
138
+ end
139
+
140
+ @@bucket_name = s3_config[:bucket_name]
141
+
142
+ Base.establish_connection!(
143
+ :access_key_id => s3_config[:access_key_id],
144
+ :secret_access_key => s3_config[:secret_access_key],
145
+ :server => s3_config[:server],
146
+ :port => s3_config[:port],
147
+ :use_ssl => s3_config[:use_ssl]
148
+ )
149
+
150
+ # Bucket.create(@@bucket_name)
151
+
152
+ base.before_update :rename_file
153
+ end
154
+
155
+ def self.protocol
156
+ @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
157
+ end
158
+
159
+ def self.hostname
160
+ @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
161
+ end
162
+
163
+ def self.port_string
164
+ @port_string ||= s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80) ? '' : ":#{s3_config[:port]}"
165
+ end
166
+
167
+ module ClassMethods
168
+ def s3_protocol
169
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
170
+ end
171
+
172
+ def s3_hostname
173
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
174
+ end
175
+
176
+ def s3_port_string
177
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
178
+ end
179
+ end
180
+
181
+ # Overwrites the base filename writer in order to store the old filename
182
+ def filename=(value)
183
+ @old_filename = filename unless filename.nil? || @old_filename
184
+ write_attribute :filename, sanitize_filename(value)
185
+ end
186
+
187
+ # The attachment ID used in the full path of a file
188
+ def attachment_path_id
189
+ ((respond_to?(:parent_id) && parent_id) || id).to_s
190
+ end
191
+
192
+ # The pseudo hierarchy containing the file relative to the bucket name
193
+ # Example: <tt>:table_name/:id</tt>
194
+ def base_path
195
+ File.join(attachment_options[:path_prefix], attachment_path_id)
196
+ end
197
+
198
+ # The full path to the file relative to the bucket name
199
+ # Example: <tt>:table_name/:id/:filename</tt>
200
+ def full_filename(thumbnail = nil)
201
+ File.join(base_path, thumbnail_name_for(thumbnail))
202
+ end
203
+
204
+ # All public objects are accessible via a GET request to the S3 servers. You can generate a
205
+ # url for an object using the s3_url method.
206
+ #
207
+ # @photo.s3_url
208
+ #
209
+ # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
210
+ # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
211
+ # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
212
+ #
213
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
214
+ def s3_url(thumbnail = nil)
215
+ File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
216
+ end
217
+ alias :public_filename :s3_url
218
+
219
+ # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
220
+ # authenticated url for an object like this:
221
+ #
222
+ # @photo.authenticated_s3_url
223
+ #
224
+ # By default authenticated urls expire 5 minutes after they were generated.
225
+ #
226
+ # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
227
+ # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
228
+ #
229
+ # # Absolute expiration date (October 13th, 2025)
230
+ # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
231
+ #
232
+ # # Expiration in five hours from now
233
+ # @photo.authenticated_s3_url(:expires_in => 5.hours)
234
+ #
235
+ # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
236
+ # By default, the ssl settings for the current connection will be used:
237
+ #
238
+ # @photo.authenticated_s3_url(:use_ssl => true)
239
+ #
240
+ # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
241
+ #
242
+ # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
243
+ def authenticated_s3_url(*args)
244
+ thumbnail = args.first.is_a?(String) ? args.first : nil
245
+ options = args.last.is_a?(Hash) ? args.last : {}
246
+ S3Object.url_for(full_filename(thumbnail), bucket_name, options)
247
+ end
248
+
249
+ def create_temp_file
250
+ write_to_temp_file current_data
251
+ end
252
+
253
+ def current_data
254
+ S3Object.value full_filename, bucket_name
255
+ end
256
+
257
+ def s3_protocol
258
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
259
+ end
260
+
261
+ def s3_hostname
262
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
263
+ end
264
+
265
+ def s3_port_string
266
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
267
+ end
268
+
269
+ protected
270
+ # Called in the after_destroy callback
271
+ def destroy_file
272
+ S3Object.delete full_filename, bucket_name
273
+ end
274
+
275
+ def rename_file
276
+ return unless @old_filename && @old_filename != filename
277
+
278
+ old_full_filename = File.join(base_path, @old_filename)
279
+
280
+ S3Object.rename(
281
+ old_full_filename,
282
+ full_filename,
283
+ bucket_name,
284
+ :access => attachment_options[:s3_access]
285
+ )
286
+
287
+ @old_filename = nil
288
+ true
289
+ end
290
+
291
+ def save_to_storage
292
+ if save_attachment?
293
+ S3Object.store(
294
+ full_filename,
295
+ (temp_path ? File.open(temp_path) : temp_data),
296
+ bucket_name,
297
+ :content_type => content_type,
298
+ :access => attachment_options[:s3_access]
299
+ )
300
+ end
301
+
302
+ @old_filename = nil
303
+ true
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,6 @@
1
+ namespace :attachmerb_fu do
2
+ desc "Do something for attachmerb_fu"
3
+ task :default do
4
+ puts "attachmerb_fu doesn't do anything"
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ require 'image_science'
2
+
3
+ module AttachmerbFu # :nodoc:
4
+ module Processors
5
+ module ImageScienceProcessor
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.alias_method_chain :process_attachment, :processing
9
+ end
10
+
11
+ module ClassMethods
12
+ # Yields a block containing an RMagick Image for the given binary data.
13
+ def with_image(file, &block)
14
+ ::ImageScience.with_image file, &block
15
+ end
16
+ end
17
+
18
+ protected
19
+ def process_attachment_with_processing
20
+ return unless process_attachment_without_processing && image?
21
+ with_image do |img|
22
+ self.width = img.width if respond_to?(:width)
23
+ self.height = img.height if respond_to?(:height)
24
+ resize_image_or_thumbnail! img
25
+ end
26
+ end
27
+
28
+ # Performs the actual resizing operation for a thumbnail
29
+ def resize_image(img, size)
30
+ # create a dummy temp file to write to
31
+ # ImageScience doesn't handle all gifs properly, so it converts them to
32
+ # pngs for thumbnails. It has something to do with trying to save gifs
33
+ # with a larger palette than 256 colors, which is all the gif format
34
+ # supports.
35
+ filename.sub! /gif$/, 'png'
36
+ content_type.sub!(/gif$/, 'png')
37
+ self.temp_path = write_to_temp_file(filename)
38
+ grab_dimensions = lambda do |img|
39
+ self.width = img.width if respond_to?(:width)
40
+ self.height = img.height if respond_to?(:height)
41
+ img.save self.temp_path
42
+ self.size = File.size(self.temp_path)
43
+ callback_with_args :after_resize, img
44
+ end
45
+
46
+ size = size.first if size.is_a?(Array) && size.length == 1
47
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
48
+ if size.is_a?(Fixnum)
49
+ img.thumbnail(size, &grab_dimensions)
50
+ else
51
+ img.resize(size[0], size[1], &grab_dimensions)
52
+ end
53
+ else
54
+ new_size = [img.width, img.height] / size.to_s
55
+ img.resize(new_size[0], new_size[1], &grab_dimensions)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end