attachmerb_fu 0.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.
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