file_column_with_s3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ module FileColumn
2
+ module AttachementStore
3
+ begin
4
+ require 'aws-sdk'
5
+ class S3Store
6
+ def initialize(options)
7
+ s3 = AWS::S3.new(:access_key_id => options[:access_key_id],
8
+ :secret_access_key => options[:secret_access_key])
9
+ @bucket = s3.buckets[options[:bucket_name]]
10
+ end
11
+
12
+ def upload(path, local_file)
13
+ @bucket.objects.create('/' + path + "/" + File.basename(local_file), File.read(local_file))
14
+ end
15
+
16
+ def upload_dir(path, local_dir)
17
+ @bucket.objects.with_prefix("/" + path).delete_all
18
+ Dir[File.join(local_dir, "*")].each do |f|
19
+ upload(path, f)
20
+ end
21
+ end
22
+
23
+ def read(path)
24
+ @bucket.objects["/" + path ].read
25
+ end
26
+
27
+ def exists?(path)
28
+ @bucket.objects['/' + path].exists?
29
+ end
30
+
31
+ def clear
32
+ @bucket.clear!
33
+ end
34
+ end
35
+ rescue LoadError => e
36
+ puts "Warning: can not load aws-sdk gem, s3 file store disabled"
37
+ end
38
+
39
+ class FilesystemStore
40
+ def initialize(dir)
41
+ @dir = dir
42
+ FileUtils.mkdir_p @dir
43
+ end
44
+
45
+ def read(path)
46
+ File.read(absolute_path(path))
47
+ end
48
+
49
+ def upload(path, local_file)
50
+ FileUtils.mkdir_p(absolute_path(path))
51
+ FileUtils.mv(local_file, absolute_path(path))
52
+ end
53
+
54
+ def upload_dir(path, local_dir)
55
+ FileUtils.rm_rf(absolute_path(path))
56
+ Dir[File.join(local_dir, "*")].each do |f|
57
+ upload(path, f)
58
+ end
59
+ end
60
+
61
+ #todo: this should be interface that retrive a lazy file object
62
+ def absolute_path(*relative_paths)
63
+ File.join(@dir, *relative_paths)
64
+ end
65
+
66
+ def exists?(file_path)
67
+ File.exists?(absolute_path(file_path))
68
+ end
69
+
70
+ def clear
71
+ FileUtils.rm_rf @dir
72
+ end
73
+ end
74
+
75
+ class Builder
76
+ def initialize(*build_opts)
77
+ @type, *@build_opts = *build_opts
78
+ end
79
+
80
+ def build(dir=nil)
81
+ args = @build_opts
82
+ args += [dir] if @type == :filesystem
83
+ store_class.new(*args)
84
+ end
85
+
86
+ private
87
+
88
+ def store_class
89
+ ActiveSupport::Inflector.constantize("FileColumn::AttachementStore::#{ActiveSupport::Inflector.camelize(@type)}Store")
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,150 @@
1
+ # This module contains helper methods for displaying and uploading files
2
+ # for attributes created by +FileColumn+'s +file_column+ method. It will be
3
+ # automatically included into ActionView::Base, thereby making this module's
4
+ # methods available in all your views.
5
+ module FileColumnHelper
6
+
7
+ # Use this helper to create an upload field for a file_column attribute. This will generate
8
+ # an additional hidden field to keep uploaded files during form-redisplays. For example,
9
+ # when called with
10
+ #
11
+ # <%= file_column_field("entry", "image") %>
12
+ #
13
+ # the following HTML will be generated (assuming the form is redisplayed and something has
14
+ # already been uploaded):
15
+ #
16
+ # <input type="hidden" name="entry[image_temp]" value="..." />
17
+ # <input type="file" name="entry[image]" />
18
+ #
19
+ # You can use the +option+ argument to pass additional options to the file-field tag.
20
+ #
21
+ # Be sure to set the enclosing form's encoding to 'multipart/form-data', by
22
+ # using something like this:
23
+ #
24
+ # <%= form_tag {:action => "create", ...}, :multipart => true %>
25
+ def file_column_field(object, method, options={})
26
+ result = ActionView::Helpers::InstanceTag.new(object.dup, method.to_s+"_temp", self).to_input_field_tag("hidden", {})
27
+ result << ActionView::Helpers::InstanceTag.new(object.dup, method, self).to_input_field_tag("file", options)
28
+ end
29
+
30
+ # Creates an URL where an uploaded file can be accessed. When called for an Entry object with
31
+ # id 42 (stored in <tt>@entry</tt>) like this
32
+ #
33
+ # <%= url_for_file_column(@entry, "image")
34
+ #
35
+ # the following URL will be produced, assuming the file "test.png" has been stored in
36
+ # the "image"-column of an Entry object stored in <tt>@entry</tt>:
37
+ #
38
+ # /entry/image/42/test.png
39
+ #
40
+ # This will produce a valid URL even for temporary uploaded files, e.g. files where the object
41
+ # they are belonging to has not been saved in the database yet.
42
+ #
43
+ # The URL produces, although starting with a slash, will be relative
44
+ # to your app's root. If you pass it to one rails' +image_tag+
45
+ # helper, rails will properly convert it to an absolute
46
+ # URL. However, this will not be the case, if you create a link with
47
+ # the +link_to+ helper. In this case, you can pass <tt>:absolute =>
48
+ # true</tt> to +options+, which will make sure, the generated URL is
49
+ # absolute on your server. Examples:
50
+ #
51
+ # <%= image_tag url_for_file_column(@entry, "image") %>
52
+ # <%= link_to "Download", url_for_file_column(@entry, "image", :absolute => true) %>
53
+ #
54
+ # If there is currently no uploaded file stored in the object's column this method will
55
+ # return +nil+.
56
+ def url_for_file_column(object, method, options=nil)
57
+ case object
58
+ when String, Symbol
59
+ object = instance_variable_get("@#{object.to_s}")
60
+ end
61
+
62
+ # parse options
63
+ subdir = nil
64
+ absolute = false
65
+ if options
66
+ case options
67
+ when Hash
68
+ subdir = options[:subdir]
69
+ absolute = options[:absolute]
70
+ when String, Symbol
71
+ subdir = options
72
+ end
73
+ end
74
+
75
+ relative_path = object.send("#{method}_relative_path", subdir)
76
+ return nil unless relative_path
77
+
78
+ url = ""
79
+ url << ActionController::Base.relative_url_root.to_s if absolute
80
+ url << "/"
81
+ url << object.send("#{method}_options")[:base_url] << "/"
82
+ url << relative_path
83
+ end
84
+
85
+ # Same as +url_for_file_colum+ but allows you to access different versions
86
+ # of the image that have been processed by RMagick.
87
+ #
88
+ # If your +options+ parameter is non-nil this will
89
+ # access a different version of an image that will be produced by
90
+ # RMagick. You can use the following types for +options+:
91
+ #
92
+ # * a <tt>:symbol</tt> will select a version defined in the model
93
+ # via FileColumn::Magick's <tt>:versions</tt> feature.
94
+ # * a <tt>geometry_string</tt> will dynamically create an
95
+ # image resized as specified by <tt>geometry_string</tt>. The image will
96
+ # be stored so that it does not have to be recomputed the next time the
97
+ # same version string is used.
98
+ # * <tt>some_hash</tt> will dynamically create an image
99
+ # that is created according to the options in <tt>some_hash</tt>. This
100
+ # accepts exactly the same options as Magick's version feature.
101
+ #
102
+ # The version produced by RMagick will be stored in a special sub-directory.
103
+ # The directory's name will be derived from the options you specified
104
+ # (via a hash function) but if you want
105
+ # to set it yourself, you can use the <tt>:name => name</tt> option.
106
+ #
107
+ # Examples:
108
+ #
109
+ # <%= url_for_image_column @entry, "image", "640x480" %>
110
+ #
111
+ # will produce an URL like this
112
+ #
113
+ # /entry/image/42/bdn19n/filename.jpg
114
+ # # "640x480".hash.abs.to_s(36) == "bdn19n"
115
+ #
116
+ # and
117
+ #
118
+ # <%= url_for_image_column @entry, "image",
119
+ # :size => "50x50", :crop => "1:1", :name => "thumb" %>
120
+ #
121
+ # will produce something like this:
122
+ #
123
+ # /entry/image/42/thumb/filename.jpg
124
+ #
125
+ # Hint: If you are using the same geometry string / options hash multiple times, you should
126
+ # define it in a helper to stay with DRY. Another option is to define it in the model via
127
+ # FileColumn::Magick's <tt>:versions</tt> feature and then refer to it via a symbol.
128
+ #
129
+ # The URL produced by this method is relative to your application's root URL,
130
+ # although it will start with a slash.
131
+ # If you pass this URL to rails' +image_tag+ helper, it will be converted to an
132
+ # absolute URL automatically.
133
+ # If there is currently no image uploaded, or there is a problem while loading
134
+ # the image this method will return +nil+.
135
+ def url_for_image_column(object, method, options=nil)
136
+ case object
137
+ when String, Symbol
138
+ object = instance_variable_get("@#{object.to_s}")
139
+ end
140
+ subdir = nil
141
+ if options
142
+ subdir = object.send("#{method}_state").create_magick_version_if_needed(options)
143
+ end
144
+ if subdir.nil?
145
+ nil
146
+ else
147
+ url_for_file_column(object, method, subdir)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,28 @@
1
+ module FileColumn
2
+
3
+ # This bit of code allows you to pass regular old files to
4
+ # file_column. file_column depends on a few extra methods that the
5
+ # CGI uploaded file class adds. We will add the equivalent methods
6
+ # to file objects if necessary by extending them with this module. This
7
+ # avoids opening up the standard File class which might result in
8
+ # naming conflicts.
9
+
10
+ module FileCompat # :nodoc:
11
+ def original_filename
12
+ File.basename(path)
13
+ end
14
+
15
+ def size
16
+ File.size(path)
17
+ end
18
+
19
+ def local_path
20
+ path
21
+ end
22
+
23
+ def content_type
24
+ nil
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,265 @@
1
+ module FileColumn # :nodoc:
2
+
3
+ class BaseUploadedFile # :nodoc:
4
+ def transform_with_magick
5
+ if needs_transform?
6
+ begin
7
+ img = ::Magick::Image::read(absolute_path).first
8
+ rescue ::Magick::ImageMagickError
9
+ if options[:magick][:image_required]
10
+ @magick_errors ||= []
11
+ @magick_errors << "invalid image"
12
+ end
13
+ return
14
+ end
15
+
16
+ if options[:magick][:versions]
17
+ options[:magick][:versions].each_pair do |version, version_options|
18
+ next if version_options[:lazy]
19
+ dirname = version_options[:name]
20
+ FileUtils.mkdir File.join(@dir, dirname)
21
+ transform_image(img, version_options, absolute_path(dirname))
22
+ end
23
+ end
24
+ if options[:magick][:size] or options[:magick][:crop] or options[:magick][:transformation] or options[:magick][:attributes]
25
+ transform_image(img, options[:magick], absolute_path)
26
+ end
27
+
28
+ GC.start
29
+ end
30
+ end
31
+
32
+ def create_magick_version_if_needed(version)
33
+ # RMagick might not have been loaded so far.
34
+ # We do not want to require it on every call of this method
35
+ # as this might be fairly expensive, so we just try if ::Magick
36
+ # exists and require it if not.
37
+ begin
38
+ ::Magick
39
+ rescue NameError
40
+ require 'RMagick'
41
+ end
42
+
43
+ if version.is_a?(Symbol)
44
+ version_options = options[:magick][:versions][version]
45
+ else
46
+ version_options = MagickExtension::process_options(version)
47
+ end
48
+
49
+ unless File.exists?(absolute_path(version_options[:name]))
50
+ begin
51
+ img = ::Magick::Image::read(absolute_path).first
52
+ rescue ::Magick::ImageMagickError
53
+ # we might be called directly from the view here
54
+ # so we just return nil if we cannot load the image
55
+ return nil
56
+ end
57
+ dirname = version_options[:name]
58
+ FileUtils.mkdir File.join(@dir, dirname)
59
+ transform_image(img, version_options, absolute_path(dirname))
60
+ end
61
+
62
+ version_options[:name]
63
+ end
64
+
65
+ attr_reader :magick_errors
66
+
67
+ def has_magick_errors?
68
+ @magick_errors and !@magick_errors.empty?
69
+ end
70
+
71
+ private
72
+
73
+ def needs_transform?
74
+ options[:magick] and just_uploaded? and
75
+ (options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes])
76
+ end
77
+
78
+ def transform_image(img, img_options, dest_path)
79
+ begin
80
+ if img_options[:transformation]
81
+ if img_options[:transformation].is_a?(Symbol)
82
+ img = @instance.send(img_options[:transformation], img)
83
+ else
84
+ img = img_options[:transformation].call(img)
85
+ end
86
+ end
87
+ if img_options[:crop]
88
+ dx, dy = img_options[:crop].split(':').map { |x| x.to_f }
89
+ w, h = (img.rows * dx / dy), (img.columns * dy / dx)
90
+ img = img.crop(::Magick::CenterGravity, [img.columns, w].min,
91
+ [img.rows, h].min, true)
92
+ end
93
+
94
+ if img_options[:size]
95
+ img = img.change_geometry(img_options[:size]) do |c, r, i|
96
+ i.resize(c, r)
97
+ end
98
+ end
99
+
100
+ if img_options[:letterbox]
101
+ w, h = img_options[:letterbox].split('x').map { |x| x.to_f }
102
+ img = img.crop((img.columns/2 - w/2), (img.rows/2 - h/2), w, h)
103
+ end
104
+ ensure
105
+ img.write(dest_path) do
106
+ if img_options[:attributes]
107
+ img_options[:attributes].each_pair do |property, value|
108
+ self.send "#{property}=", value
109
+ end
110
+ end
111
+ end
112
+ File.chmod options[:permissions], dest_path
113
+ end
114
+ end
115
+ end
116
+
117
+ # If you are using file_column to upload images, you can
118
+ # directly process the images with RMagick,
119
+ # a ruby extension
120
+ # for accessing the popular imagemagick libraries. You can find
121
+ # more information about RMagick at http://rmagick.rubyforge.org.
122
+ #
123
+ # You can control what to do by adding a <tt>:magick</tt> option
124
+ # to your options hash. All operations are performed immediately
125
+ # after a new file is assigned to the file_column attribute (i.e.,
126
+ # when a new file has been uploaded).
127
+ #
128
+ # == Resizing images
129
+ #
130
+ # To resize the uploaded image according to an imagemagick geometry
131
+ # string, just use the <tt>:size</tt> option:
132
+ #
133
+ # file_column :image, :magick => {:size => "800x600>"}
134
+ #
135
+ # If the uploaded file cannot be loaded by RMagick, file_column will
136
+ # signal a validation error for the corresponding attribute. If you
137
+ # want to allow non-image files to be uploaded in a column that uses
138
+ # the <tt>:magick</tt> option, you can set the <tt>:image_required</tt>
139
+ # attribute to +false+:
140
+ #
141
+ # file_column :image, :magick => {:size => "800x600>",
142
+ # :image_required => false }
143
+ #
144
+ # == Multiple versions
145
+ #
146
+ # You can also create additional versions of your image, for example
147
+ # thumb-nails, like this:
148
+ # file_column :image, :magick => {:versions => {
149
+ # :thumb => {:size => "50x50"},
150
+ # :medium => {:size => "640x480>"}
151
+ # }
152
+ #
153
+ # These versions will be stored in separate sub-directories, named like the
154
+ # symbol you used to identify the version. So in the previous example, the
155
+ # image versions will be stored in "thumb", "screen" and "widescreen"
156
+ # directories, resp.
157
+ # A name different from the symbol can be set via the <tt>:name</tt> option.
158
+ #
159
+ # These versions can be accessed via FileColumnHelper's +url_for_image_column+
160
+ # method like this:
161
+ #
162
+ # <%= url_for_image_column "entry", "image", :thumb %>
163
+ #
164
+ # == Cropping images
165
+ #
166
+ # If you wish to crop your images with a size ratio before scaling
167
+ # them according to your version geometry, you can use the :crop directive.
168
+ # file_column :image, :magick => {:versions => {
169
+ # :square => {:crop => "1:1", :size => "50x50", :name => "thumb"},
170
+ # :screen => {:crop => "4:3", :size => "640x480>"},
171
+ # :widescreen => {:crop => "16:9", :size => "640x360!"},
172
+ # }
173
+ # }
174
+ #
175
+ # == Custom attributes
176
+ #
177
+ # To change some of the image properties like compression level before they
178
+ # are saved you can set the <tt>:attributes</tt> option.
179
+ # For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html
180
+ #
181
+ # file_column :image, :magick => { :attributes => { :quality => 30 } }
182
+ #
183
+ # == Custom transformations
184
+ #
185
+ # To perform custom transformations on uploaded images, you can pass a
186
+ # callback to file_column:
187
+ # file_column :image, :magick =>
188
+ # Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }
189
+ #
190
+ # The callback you give, receives one argument, which is an instance
191
+ # of Magick::Image, the RMagick image class. It should return a transformed
192
+ # image. Instead of passing a <tt>Proc</tt> object, you can also give a
193
+ # <tt>Symbol</tt>, the name of an instance method of your model.
194
+ #
195
+ # Custom transformations can be combined via the standard :size and :crop
196
+ # features, by using the :transformation option:
197
+ # file_column :image, :magick => {
198
+ # :transformation => Proc.new { |image| ... },
199
+ # :size => "640x480"
200
+ # }
201
+ #
202
+ # In this case, the standard resizing operations will be performed after the
203
+ # custom transformation.
204
+ #
205
+ # Of course, custom transformations can be used in versions, as well.
206
+ #
207
+ # <b>Note:</b> You'll need the
208
+ # RMagick extension being installed in order to use file_column's
209
+ # imagemagick integration.
210
+ module MagickExtension
211
+
212
+ def self.file_column(klass, attr, options) # :nodoc:
213
+ require 'RMagick'
214
+ options[:magick] = process_options(options[:magick],false) if options[:magick]
215
+ if options[:magick][:versions]
216
+ options[:magick][:versions].each_pair do |name, value|
217
+ options[:magick][:versions][name] = process_options(value, name.to_s)
218
+ end
219
+ end
220
+ state_method = "#{attr}_state".to_sym
221
+ after_assign_method = "#{attr}_magick_after_assign".to_sym
222
+
223
+ klass.send(:define_method, after_assign_method) do
224
+ self.send(state_method).transform_with_magick
225
+ end
226
+
227
+ options[:after_upload] ||= []
228
+ options[:after_upload] << after_assign_method
229
+
230
+ klass.validate do |record|
231
+ state = record.send(state_method)
232
+ if state.has_magick_errors?
233
+ state.magick_errors.each do |error|
234
+ record.errors.add attr, error
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+
241
+ def self.process_options(options,create_name=true)
242
+ case options
243
+ when String then options = {:size => options}
244
+ when Proc, Symbol then options = {:transformation => options }
245
+ end
246
+ if options[:geometry]
247
+ options[:size] = options.delete(:geometry)
248
+ end
249
+ options[:image_required] = true unless options.key?(:image_required)
250
+ if options[:name].nil? and create_name
251
+ if create_name == true
252
+ hash = 0
253
+ for key in [:size, :crop]
254
+ hash = hash ^ options[key].hash if options[key]
255
+ end
256
+ options[:name] = hash.abs.to_s(36)
257
+ else
258
+ options[:name] = create_name
259
+ end
260
+ end
261
+ options
262
+ end
263
+
264
+ end
265
+ end