file_column 0.3.2

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.
data/CHANGELOG ADDED
@@ -0,0 +1,69 @@
1
+ *svn*
2
+ * allow for directories in file_column dirs as well
3
+ * use subdirs for versions instead of fiddling with filename
4
+ * url_for_image_column_helper for dynamic resizing of images from views
5
+ * new "crop" feature [Sean Treadway]
6
+ * url_for_file_column helper: do not require model objects to be stored in
7
+ instance variables
8
+ * allow more fined-grained control over :store_dir via callback
9
+ methods [Gerret Apelt]
10
+ * allow assignment of regular file objects
11
+ * validation of file format and file size [Kyle Maxwell]
12
+ * validation of image dimensions [Lee O'Mara]
13
+ * file permissions can be set via :permissions option
14
+ * fixed bug that prevents deleting of file via assigning nil if
15
+ column is declared as NON NULL on some databases
16
+ * don't expand absolute paths. This is necessary for file_column to work
17
+ when your rails app is deployed into a sub-directory via a symbolic link
18
+ * url_for_*_column will no longer return absolute URLs! Instead, although the
19
+ generated URL starts with a slash, it will be relative to your application's
20
+ root URL. This is so, because rails' image_tag helper will automatically
21
+ convert it to an absolute URL. If you need an absolute URL (e.g., to pass
22
+ it to link_to) use url_for_file_column's :absolute => true option.
23
+ * added support for file_column enabled unit tests [Manuel Holtgrewe]
24
+ * support for custom transformation of images [Frederik Fix]
25
+ * allow setting of image attributes (e.g., quality) [Frederik Fix]
26
+ * :magick columns can optionally ignore non-images (i.e., do not try to
27
+ resize them)
28
+
29
+ 0.3.1
30
+ * make object with file_columns serializable
31
+ * use normal require for RMagick, so that it works with gem
32
+ and custom install as well
33
+
34
+ 0.3
35
+ * fixed bug where empty file uploads were not recognized with some browsers
36
+ * fixed bug on windows when "file" utility is not present
37
+ * added option to disable automatic file extension correction
38
+ * Only allow one attribute per call to file_column, so that options only
39
+ apply to one argument
40
+ * try to detect when people forget to set the form encoding to
41
+ 'multipart/form-data'
42
+ * converted to rails plugin
43
+ * easy integration with RMagick
44
+
45
+ 0.2
46
+ * complete rewrite using state pattern
47
+ * fixed sanitize filename [Michael Raidel]
48
+ * fixed bug when no file was uploaded [Michael Raidel]
49
+ * try to fix filename extensions [Michael Raidel]
50
+ * Feed absolute paths through File.expand_path to make them as simple as possible
51
+ * Make file_column_field helper work with auto-ids (e.g., "event[]")
52
+
53
+ 0.1.3
54
+ * test cases with more than 1 file_column
55
+ * fixed bug when file_column was called with several arguments
56
+ * treat empty ("") file_columns as nil
57
+ * support for binary files on windows
58
+
59
+ 0.1.2
60
+ * better rails integration, so that you do not have to include the modules yourself. You
61
+ just have to "require 'rails_file_column'" in your "config/environment.rb"
62
+ * Rakefile for testing and packaging
63
+
64
+ 0.1.1 (2005-08-11)
65
+ * fixed nasty bug in url_for_file_column that made it unusable on Apache
66
+ * prepared for public release
67
+
68
+ 0.1 (2005-08-10)
69
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "jeweler", "~> 1.6.4"
10
+ end
data/README ADDED
@@ -0,0 +1,54 @@
1
+ FEATURES
2
+ ========
3
+
4
+ Let's assume an model class named Entry, where we want to define the "image" column
5
+ as a "file_upload" column.
6
+
7
+ class Entry < ActiveRecord::Base
8
+ file_column :image
9
+ end
10
+
11
+ * every entry can have one uploaded file, the filename will be stored in the "image" column
12
+
13
+ * files will be stored in "public/entry/image/<entry.id>/filename.ext"
14
+
15
+ * Newly uploaded files will be stored in "public/entry/tmp/<random>/filename.ext" so that
16
+ they can be reused in form redisplays (due to validation etc.)
17
+
18
+ * in a view, "<%= file_column_field 'entry', 'image' %> will create a file upload field as well
19
+ as a hidden field to recover files uploaded before in a case of a form redisplay
20
+
21
+ * in a view, "<%= url_for_file_column 'entry', 'image' %> will create an URL to access the
22
+ uploaded file. Note that you need an Entry object in the instance variable @entry for this
23
+ to work.
24
+
25
+ * easy integration with RMagick to resize images and/or create thumb-nails.
26
+
27
+ USAGE
28
+ =====
29
+
30
+ Just drop the whole directory into your application's "vendor/plugins" directory. Starting
31
+ with version 1.0rc of rails, it will be automatically picked for you by rails plugin
32
+ mechanism.
33
+
34
+ DOCUMENTATION
35
+ =============
36
+
37
+ Please look at the rdoc-generated documentation in the "doc" directory.
38
+
39
+ RUNNING UNITTESTS
40
+ =================
41
+
42
+ There are extensive unittests in the "test" directory. Currently, only MySQL is supported, but
43
+ you should be able to easily fix this by looking at "connection.rb". You have to create a
44
+ database for the tests and put the connection information into "connection.rb". The schema
45
+ for MySQL can be found in "test/fixtures/mysql.sql".
46
+
47
+ You can run the tests by starting the "*_test.rb" in the directory "test"
48
+
49
+ BUGS & FEEDBACK
50
+ ===============
51
+
52
+ Bug reports (as well as patches) and feedback are very welcome. Please send it to
53
+ sebastian.kanthak@muehlheim.de
54
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ task :default => [:test]
2
+
3
+ PKG_NAME = "file-column"
4
+ PKG_VERSION = "0.3.2"
5
+
6
+ PKG_DIR = "release/#{PKG_NAME}-#{PKG_VERSION}"
7
+
8
+ task :clean do
9
+ rm_rf "release"
10
+ end
11
+
12
+ task :setup_directories do
13
+ mkpath "release"
14
+ end
15
+
16
+
17
+ task :checkout_release => :setup_directories do
18
+ rm_rf PKG_DIR
19
+ revision = ENV["REVISION"] || "HEAD"
20
+ sh "svn export -r #{revision} . #{PKG_DIR}"
21
+ end
22
+
23
+ task :release_docs => :checkout_release do
24
+ sh "cd #{PKG_DIR}; rdoc lib"
25
+ end
26
+
27
+ task :package => [:checkout_release, :release_docs] do
28
+ sh "cd release; tar czf #{PKG_NAME}-#{PKG_VERSION}.tar.gz #{PKG_NAME}-#{PKG_VERSION}"
29
+ end
30
+
31
+ task :test do
32
+ sh "cd test; ruby file_column_test.rb"
33
+ sh "cd test; ruby file_column_helper_test.rb"
34
+ sh "cd test; ruby magick_test.rb"
35
+ sh "cd test; ruby magick_view_only_test.rb"
36
+ end
37
+
38
+ require 'jeweler'
39
+ Jeweler::Tasks.new do |gem|
40
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
41
+ gem.name = "file_column"
42
+ gem.homepage = "http://github.com/tekin/file_column"
43
+ gem.license = "MIT"
44
+ gem.summary = %Q{file upload and rmagick image resizing}
45
+ gem.description = %Q{file upload and rmagick image resizing}
46
+ gem.email = "sebastian.kanthak@muehlheim.de"
47
+ gem.authors = ["Sebastian Kanthak"]
48
+ # dependencies defined in Gemfile
49
+ end
50
+ Jeweler::RubygemsDotOrgTasks.new
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+ * document configuration options better
2
+ * support setting of permissions
3
+ * validation methods for file format/size
4
+ * delete stale files from tmp directories
5
+
6
+ * ensure valid URLs are created even when deployed at sub-path (compute_public_url?)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.2
data/init.rb ADDED
@@ -0,0 +1,12 @@
1
+ # plugin init file for rails
2
+ # this file will be picked up by rails automatically and
3
+ # add the file_column extensions to rails
4
+
5
+ require 'file_column'
6
+ require 'file_compat'
7
+ require 'file_column_helper'
8
+ require 'validations'
9
+
10
+ ActiveRecord::Base.send(:include, FileColumn)
11
+ ActionView::Base.send(:include, FileColumnHelper)
12
+ ActiveRecord::Base.send(:include, FileColumn::Validations)
@@ -0,0 +1,721 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ require 'magick_file_column'
4
+
5
+ module FileColumn # :nodoc:
6
+ def self.append_features(base)
7
+ super
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ def self.create_state(instance,attr)
12
+ filename = instance[attr]
13
+ if filename.nil? or filename.empty?
14
+ NoUploadedFile.new(instance,attr)
15
+ else
16
+ PermanentUploadedFile.new(instance,attr)
17
+ end
18
+ end
19
+
20
+ def self.init_options(defaults, model, attr)
21
+ options = defaults.dup
22
+ options[:store_dir] ||= File.join(options[:root_path], model, attr)
23
+ unless options[:store_dir].is_a?(Symbol)
24
+ options[:tmp_base_dir] ||= File.join(options[:store_dir], "tmp")
25
+ end
26
+ options[:base_url] ||= options[:web_root] + File.join(model, attr)
27
+
28
+ [:store_dir, :tmp_base_dir].each do |dir_sym|
29
+ if options[dir_sym].is_a?(String) and !File.exists?(options[dir_sym])
30
+ FileUtils.mkpath(options[dir_sym])
31
+ end
32
+ end
33
+
34
+ options
35
+ end
36
+
37
+ class BaseUploadedFile # :nodoc:
38
+
39
+ def initialize(instance,attr)
40
+ @instance, @attr = instance, attr
41
+ @options_method = "#{attr}_options".to_sym
42
+ end
43
+
44
+
45
+ def assign(file)
46
+ if file.is_a? File
47
+ # this did not come in via a CGI request. However,
48
+ # assigning files directly may be useful, so we
49
+ # make just this file object similar enough to an uploaded
50
+ # file that we can handle it.
51
+ file.extend FileColumn::FileCompat
52
+ end
53
+
54
+ if file.nil?
55
+ delete
56
+ else
57
+ if file.size == 0
58
+ # user did not submit a file, so we
59
+ # can simply ignore this
60
+ self
61
+ else
62
+ if file.is_a?(String)
63
+ # if file is a non-empty string it is most probably
64
+ # the filename and the user forgot to set the encoding
65
+ # to multipart/form-data. Since we would raise an exception
66
+ # because of the missing "original_filename" method anyways,
67
+ # we raise a more meaningful exception rightaway.
68
+ raise TypeError.new("Do not know how to handle a string with value '#{file}' that was passed to a file_column. Check if the form's encoding has been set to 'multipart/form-data'.")
69
+ end
70
+ upload(file)
71
+ end
72
+ end
73
+ end
74
+
75
+ def just_uploaded?
76
+ @just_uploaded
77
+ end
78
+
79
+ def on_save(&blk)
80
+ @on_save ||= []
81
+ @on_save << Proc.new
82
+ end
83
+
84
+ # the following methods are overriden by sub-classes if needed
85
+
86
+ def temp_path
87
+ nil
88
+ end
89
+
90
+ def absolute_dir
91
+ if absolute_path then File.dirname(absolute_path) else nil end
92
+ end
93
+
94
+ def relative_dir
95
+ if relative_path then File.dirname(relative_path) else nil end
96
+ end
97
+
98
+ def after_save
99
+ @on_save.each { |blk| blk.call } if @on_save
100
+ self
101
+ end
102
+
103
+ def after_destroy
104
+ end
105
+
106
+ def options
107
+ @instance.send(@options_method)
108
+ end
109
+
110
+ private
111
+
112
+ def store_dir
113
+ if options[:store_dir].is_a? Symbol
114
+ raise ArgumentError.new("'#{options[:store_dir]}' is not an instance method of class #{@instance.class.name}") unless @instance.respond_to?(options[:store_dir])
115
+
116
+ dir = File.join(options[:root_path], @instance.send(options[:store_dir]))
117
+ FileUtils.mkpath(dir) unless File.exists?(dir)
118
+ dir
119
+ else
120
+ options[:store_dir]
121
+ end
122
+ end
123
+
124
+ def tmp_base_dir
125
+ if options[:tmp_base_dir]
126
+ options[:tmp_base_dir]
127
+ else
128
+ dir = File.join(store_dir, "tmp")
129
+ FileUtils.mkpath(dir) unless File.exists?(dir)
130
+ dir
131
+ end
132
+ end
133
+
134
+ def clone_as(klass)
135
+ klass.new(@instance, @attr)
136
+ end
137
+
138
+ end
139
+
140
+
141
+ class NoUploadedFile < BaseUploadedFile # :nodoc:
142
+ def delete
143
+ # we do not have a file so deleting is easy
144
+ self
145
+ end
146
+
147
+ def upload(file)
148
+ # replace ourselves with a TempUploadedFile
149
+ temp = clone_as TempUploadedFile
150
+ temp.store_upload(file)
151
+ temp
152
+ end
153
+
154
+ def absolute_path(subdir=nil)
155
+ nil
156
+ end
157
+
158
+
159
+ def relative_path(subdir=nil)
160
+ nil
161
+ end
162
+
163
+ def assign_temp(temp_path)
164
+ return self if temp_path.nil? or temp_path.empty?
165
+ temp = clone_as TempUploadedFile
166
+ temp.parse_temp_path temp_path
167
+ temp
168
+ end
169
+ end
170
+
171
+ class RealUploadedFile < BaseUploadedFile # :nodoc:
172
+ def absolute_path(subdir=nil)
173
+ if subdir
174
+ File.join(@dir, subdir, @filename)
175
+ else
176
+ File.join(@dir, @filename)
177
+ end
178
+ end
179
+
180
+ def relative_path(subdir=nil)
181
+ if subdir
182
+ File.join(relative_path_prefix, subdir, @filename)
183
+ else
184
+ File.join(relative_path_prefix, @filename)
185
+ end
186
+ end
187
+
188
+ private
189
+
190
+ # regular expressions to try for identifying extensions
191
+ EXT_REGEXPS = [
192
+ /^(.+)\.([^.]+\.[^.]+)$/, # matches "something.tar.gz"
193
+ /^(.+)\.([^.]+)$/ # matches "something.jpg"
194
+ ]
195
+
196
+ def split_extension(filename,fallback=nil)
197
+ EXT_REGEXPS.each do |regexp|
198
+ if filename =~ regexp
199
+ base,ext = $1, $2
200
+ return [base, ext] if options[:extensions].include?(ext.downcase)
201
+ end
202
+ end
203
+ if fallback and filename =~ EXT_REGEXPS.last
204
+ return [$1, $2]
205
+ end
206
+ [filename, ""]
207
+ end
208
+
209
+ end
210
+
211
+ class TempUploadedFile < RealUploadedFile # :nodoc:
212
+
213
+ def store_upload(file)
214
+ @tmp_dir = FileColumn.generate_temp_name
215
+ @dir = File.join(tmp_base_dir, @tmp_dir)
216
+ FileUtils.mkdir(@dir)
217
+
218
+ @filename = FileColumn::sanitize_filename(file.original_filename)
219
+ local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
220
+
221
+ # stored uploaded file into local_file_path
222
+ # If it was a Tempfile object, the temporary file will be
223
+ # cleaned up automatically, so we do not have to care for this
224
+ if file.respond_to?(:local_path) and file.local_path and File.exists?(file.local_path)
225
+ FileUtils.copy_file(file.local_path, local_file_path)
226
+ elsif file.respond_to?(:read)
227
+ File.open(local_file_path, "wb") { |f| f.write(file.read) }
228
+ else
229
+ raise ArgumentError.new("Do not know how to handle #{file.inspect}")
230
+ end
231
+ File.chmod(options[:permissions], local_file_path)
232
+
233
+ if options[:fix_file_extensions]
234
+ # try to determine correct file extension and fix
235
+ # if necessary
236
+ content_type = get_content_type((file.content_type.chomp if file.content_type))
237
+ if content_type and options[:mime_extensions][content_type]
238
+ @filename = correct_extension(@filename,options[:mime_extensions][content_type])
239
+ end
240
+
241
+ new_local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
242
+ File.rename(local_file_path, new_local_file_path) unless new_local_file_path == local_file_path
243
+ local_file_path = new_local_file_path
244
+ end
245
+
246
+ @instance[@attr] = @filename
247
+ @just_uploaded = true
248
+ end
249
+
250
+
251
+ # tries to identify and strip the extension of filename
252
+ # if an regular expresion from EXT_REGEXPS matches and the
253
+ # downcased extension is a known extension (in options[:extensions])
254
+ # we'll strip this extension
255
+ def strip_extension(filename)
256
+ split_extension(filename).first
257
+ end
258
+
259
+ def correct_extension(filename, ext)
260
+ strip_extension(filename) << ".#{ext}"
261
+ end
262
+
263
+ def parse_temp_path(temp_path, instance_options=nil)
264
+ raise ArgumentError.new("invalid format of '#{temp_path}'") unless temp_path =~ %r{^((\d+\.)+\d+)/([^/].+)$}
265
+ @tmp_dir, @filename = $1, FileColumn.sanitize_filename($3)
266
+ @dir = File.join(tmp_base_dir, @tmp_dir)
267
+
268
+ @instance[@attr] = @filename unless instance_options == :ignore_instance
269
+ end
270
+
271
+ def upload(file)
272
+ # store new file
273
+ temp = clone_as TempUploadedFile
274
+ temp.store_upload(file)
275
+
276
+ # delete old copy
277
+ delete_files
278
+
279
+ # and return new TempUploadedFile object
280
+ temp
281
+ end
282
+
283
+ def delete
284
+ delete_files
285
+ @instance[@attr] = ""
286
+ clone_as NoUploadedFile
287
+ end
288
+
289
+ def assign_temp(temp_path)
290
+ return self if temp_path.nil? or temp_path.empty?
291
+ # we can ignore this since we've already received a newly uploaded file
292
+
293
+ # however, we delete the old temporary files
294
+ temp = clone_as TempUploadedFile
295
+ temp.parse_temp_path(temp_path, :ignore_instance)
296
+ temp.delete_files
297
+
298
+ self
299
+ end
300
+
301
+ def temp_path
302
+ File.join(@tmp_dir, @filename)
303
+ end
304
+
305
+ def after_save
306
+ super
307
+
308
+ # we have a newly uploaded image, move it to the correct location
309
+ file = clone_as PermanentUploadedFile
310
+ file.move_from(File.join(tmp_base_dir, @tmp_dir), @just_uploaded)
311
+
312
+ # delete temporary files
313
+ delete_files
314
+
315
+ # replace with the new PermanentUploadedFile object
316
+ file
317
+ end
318
+
319
+ def delete_files
320
+ FileUtils.rm_rf(File.join(tmp_base_dir, @tmp_dir))
321
+ end
322
+
323
+ def get_content_type(fallback=nil)
324
+ if options[:file_exec]
325
+ begin
326
+ content_type = `#{options[:file_exec]} -bi "#{File.join(@dir,@filename)}"`.chomp
327
+ content_type = fallback unless $?.success?
328
+ content_type.gsub!(/;.+$/,"") if content_type
329
+ content_type
330
+ rescue
331
+ fallback
332
+ end
333
+ else
334
+ fallback
335
+ end
336
+ end
337
+
338
+ private
339
+
340
+ def relative_path_prefix
341
+ File.join("tmp", @tmp_dir)
342
+ end
343
+ end
344
+
345
+
346
+ class PermanentUploadedFile < RealUploadedFile # :nodoc:
347
+ def initialize(*args)
348
+ super *args
349
+ @dir = File.join(store_dir, relative_path_prefix)
350
+ @filename = @instance[@attr]
351
+ @filename = nil if @filename.empty?
352
+ FileUtils.mkpath(File.dirname(@dir)) unless File.exists?(File.dirname(@dir))
353
+ end
354
+
355
+ def move_from(local_dir, just_uploaded)
356
+ # remove old permament dir first
357
+ # this creates a short moment, where neither the old nor
358
+ # the new files exist but we can't do much about this as
359
+ # filesystems aren't transactional.
360
+ FileUtils.rm_rf @dir
361
+
362
+ FileUtils.mv local_dir, @dir
363
+
364
+ @just_uploaded = just_uploaded
365
+ end
366
+
367
+ def upload(file)
368
+ temp = clone_as TempUploadedFile
369
+ temp.store_upload(file)
370
+ temp
371
+ end
372
+
373
+ def delete
374
+ file = clone_as NoUploadedFile
375
+ @instance[@attr] = ""
376
+ file.on_save { delete_files }
377
+ file
378
+ end
379
+
380
+ def assign_temp(temp_path)
381
+ return nil if temp_path.nil? or temp_path.empty?
382
+
383
+ temp = clone_as TempUploadedFile
384
+ temp.parse_temp_path(temp_path)
385
+ temp
386
+ end
387
+
388
+ def after_destroy
389
+ delete_files
390
+ end
391
+
392
+ def delete_files
393
+ FileUtils.rm_rf @dir
394
+ end
395
+
396
+ private
397
+
398
+ def relative_path_prefix
399
+ raise RuntimeError.new("Trying to access file_column, but primary key got lost.") if @instance.id.to_s.empty?
400
+ File.join(*("%08d" % @instance.id).scan(/..../))
401
+ end
402
+ end
403
+
404
+ # The FileColumn module allows you to easily handle file uploads. You can designate
405
+ # one or more columns of your model's table as "file columns" like this:
406
+ #
407
+ # class Entry < ActiveRecord::Base
408
+ #
409
+ # file_column :image
410
+ # end
411
+ #
412
+ # Now, by default, an uploaded file "test.png" for an entry object with primary key 42 will
413
+ # be stored in in "public/entry/image/42/test.png". The filename "test.png" will be stored
414
+ # in the record's "image" column. The "entries" table should have a +VARCHAR+ column
415
+ # named "image".
416
+ #
417
+ # The methods of this module are automatically included into <tt>ActiveRecord::Base</tt>
418
+ # as class methods, so that you can use them in your models.
419
+ #
420
+ # == Generated Methods
421
+ #
422
+ # After calling "<tt>file_column :image</tt>" as in the example above, a number of instance methods
423
+ # will automatically be generated, all prefixed by "image":
424
+ #
425
+ # * <tt>Entry#image=(uploaded_file)</tt>: this will handle a newly uploaded file
426
+ # (see below). Note that
427
+ # you can simply call your upload field "entry[image]" in your view (or use the
428
+ # helper).
429
+ # * <tt>Entry#image(subdir=nil)</tt>: This will return an absolute path (as a
430
+ # string) to the currently uploaded file
431
+ # or nil if no file has been uploaded
432
+ # * <tt>Entry#image_relative_path(subdir=nil)</tt>: This will return a path relative to
433
+ # this file column's base directory
434
+ # as a string or nil if no file has been uploaded. This would be "42/test.png" in the example.
435
+ # * <tt>Entry#image_just_uploaded?</tt>: Returns true if a new file has been uploaded to this instance.
436
+ # You can use this in your code to perform certain actions (e. g., validation,
437
+ # custom post-processing) only on newly uploaded files.
438
+ #
439
+ # You can access the raw value of the "image" column (which will contain the filename) via the
440
+ # <tt>ActiveRecord::Base#attributes</tt> or <tt>ActiveRecord::Base#[]</tt> methods like this:
441
+ #
442
+ # entry['image'] # e.g."test.png"
443
+ #
444
+ # == Storage of uploaded files
445
+ #
446
+ # For a model class +Entry+ and a column +image+, all files will be stored under
447
+ # "public/entry/image". A sub-directory named after the primary key of the object will
448
+ # be created, so that files can be stored using their real filename. For example, a file
449
+ # "test.png" stored in an Entry object with id 42 will be stored in
450
+ #
451
+ # public/entry/image/42/test.png
452
+ #
453
+ # Files will be moved to this location in an +after_save+ callback. They will be stored in
454
+ # a temporary location previously as explained in the next section.
455
+ #
456
+ # By default, files will be created with unix permissions of <tt>0644</tt> (i. e., owner has
457
+ # read/write access, group and others only have read access). You can customize
458
+ # this by passing the desired mode as a <tt>:permissions</tt> options. The value
459
+ # you give here is passed directly to <tt>File::chmod</tt>, so on Unix you should
460
+ # give some octal value like 0644, for example.
461
+ #
462
+ # == Handling of form redisplay
463
+ #
464
+ # Suppose you have a form for creating a new object where the user can upload an image. The form may
465
+ # have to be re-displayed because of validation errors. The uploaded file has to be stored somewhere so
466
+ # that the user does not have to upload it again. FileColumn will store these in a temporary directory
467
+ # (called "tmp" and located under the column's base directory by default) so that it can be moved to
468
+ # the final location if the object is successfully created. If the form is never completed, though, you
469
+ # can easily remove all the images in this "tmp" directory once per day or so.
470
+ #
471
+ # So in the example above, the image "test.png" would first be stored in
472
+ # "public/entry/image/tmp/<some_random_key>/test.png" and be moved to
473
+ # "public/entry/image/<primary_key>/test.png".
474
+ #
475
+ # This temporary location of newly uploaded files has another advantage when updating objects. If the
476
+ # update fails for some reasons (e.g. due to validations), the existing image will not be overwritten, so
477
+ # it has a kind of "transactional behaviour".
478
+ #
479
+ # == Additional Files and Directories
480
+ #
481
+ # FileColumn allows you to keep more than one file in a directory and will move/delete
482
+ # all the files and directories it finds in a model object's directory when necessary.
483
+ #
484
+ # As a convenience you can access files stored in sub-directories via the +subdir+
485
+ # parameter if they have the same filename.
486
+ #
487
+ # Suppose your uploaded file is named "vancouver.jpg" and you want to create a
488
+ # thumb-nail and store it in the "thumb" directory. If you call
489
+ # <tt>image("thumb")</tt>, you
490
+ # will receive an absolute path for the file "thumb/vancouver.jpg" in the same
491
+ # directory "vancouver.jpg" is stored. Look at the documentation of FileColumn::Magick
492
+ # for more examples and how to create these thumb-nails automatically.
493
+ #
494
+ # == File Extensions
495
+ #
496
+ # FileColumn will try to fix the file extension of uploaded files, so that
497
+ # the files are served with the correct mime-type by your web-server. Most
498
+ # web-servers are setting the mime-type based on the file's extension. You
499
+ # can disable this behaviour by passing the <tt>:fix_file_extensions</tt> option
500
+ # with a value of +nil+ to +file_column+.
501
+ #
502
+ # In order to set the correct extension, FileColumn tries to determine
503
+ # the files mime-type first. It then uses the +MIME_EXTENSIONS+ hash to
504
+ # choose the corresponding file extension. You can override this hash
505
+ # by passing in a <tt>:mime_extensions</tt> option to +file_column+.
506
+ #
507
+ # The mime-type of the uploaded file is determined with the following steps:
508
+ #
509
+ # 1. Run the external "file" utility. You can specify the full path to
510
+ # the executable in the <tt>:file_exec</tt> option or set this option
511
+ # to +nil+ to disable this step
512
+ #
513
+ # 2. If the file utility couldn't determine the mime-type or the utility was not
514
+ # present, the content-type provided by the user's browser is used
515
+ # as a fallback.
516
+ #
517
+ # == Custom Storage Directories
518
+ #
519
+ # FileColumn's storage location is determined in the following way. All
520
+ # files are saved below the so-called "root_path" directory, which defaults to
521
+ # "RAILS_ROOT/public". For every file_column, you can set a separte "store_dir"
522
+ # option. It defaults to "model_name/attribute_name".
523
+ #
524
+ # Files will always be stored in sub-directories of the store_dir path. The
525
+ # subdirectory is named after the instance's +id+ attribute for a saved model,
526
+ # or "tmp/<randomkey>" for unsaved models.
527
+ #
528
+ # You can specify a custom root_path by setting the <tt>:root_path</tt> option.
529
+ #
530
+ # You can specify a custom storage_dir by setting the <tt>:storage_dir</tt> option.
531
+ #
532
+ # For setting a static storage_dir that doesn't change with respect to a particular
533
+ # instance, you assign <tt>:storage_dir</tt> a String representing a directory
534
+ # as an absolute path.
535
+ #
536
+ # If you need more fine-grained control over the storage directory, you
537
+ # can use the name of a callback-method as a symbol for the
538
+ # <tt>:store_dir</tt> option. This method has to be defined as an
539
+ # instance method in your model. It will be called without any arguments
540
+ # whenever the storage directory for an uploaded file is needed. It should return
541
+ # a String representing a directory relativeo to root_path.
542
+ #
543
+ # Uploaded files for unsaved models objects will be stored in a temporary
544
+ # directory. By default this directory will be a "tmp" directory in
545
+ # your <tt>:store_dir</tt>. You can override this via the
546
+ # <tt>:tmp_base_dir</tt> option.
547
+ module ClassMethods
548
+
549
+ # default mapping of mime-types to file extensions. FileColumn will try to
550
+ # rename a file to the correct extension if it detects a known mime-type
551
+ MIME_EXTENSIONS = {
552
+ "image/gif" => "gif",
553
+ "image/jpeg" => "jpg",
554
+ "image/pjpeg" => "jpg",
555
+ "image/x-png" => "png",
556
+ "image/jpg" => "jpg",
557
+ "image/png" => "png",
558
+ "application/x-shockwave-flash" => "swf",
559
+ "application/pdf" => "pdf",
560
+ "application/pgp-signature" => "sig",
561
+ "application/futuresplash" => "spl",
562
+ "application/msword" => "doc",
563
+ "application/postscript" => "ps",
564
+ "application/x-bittorrent" => "torrent",
565
+ "application/x-dvi" => "dvi",
566
+ "application/x-gzip" => "gz",
567
+ "application/x-ns-proxy-autoconfig" => "pac",
568
+ "application/x-shockwave-flash" => "swf",
569
+ "application/x-tgz" => "tar.gz",
570
+ "application/x-tar" => "tar",
571
+ "application/zip" => "zip",
572
+ "audio/mpeg" => "mp3",
573
+ "audio/x-mpegurl" => "m3u",
574
+ "audio/x-ms-wma" => "wma",
575
+ "audio/x-ms-wax" => "wax",
576
+ "audio/x-wav" => "wav",
577
+ "image/x-xbitmap" => "xbm",
578
+ "image/x-xpixmap" => "xpm",
579
+ "image/x-xwindowdump" => "xwd",
580
+ "text/css" => "css",
581
+ "text/html" => "html",
582
+ "text/javascript" => "js",
583
+ "text/plain" => "txt",
584
+ "text/xml" => "xml",
585
+ "video/mpeg" => "mpeg",
586
+ "video/quicktime" => "mov",
587
+ "video/x-msvideo" => "avi",
588
+ "video/x-ms-asf" => "asf",
589
+ "video/x-ms-wmv" => "wmv"
590
+ }
591
+
592
+ EXTENSIONS = Set.new MIME_EXTENSIONS.values
593
+ EXTENSIONS.merge %w(jpeg)
594
+
595
+ # default options. You can override these with +file_column+'s +options+ parameter
596
+ DEFAULT_OPTIONS = {
597
+ :root_path => File.join(RAILS_ROOT, "public"),
598
+ :web_root => "",
599
+ :mime_extensions => MIME_EXTENSIONS,
600
+ :extensions => EXTENSIONS,
601
+ :fix_file_extensions => true,
602
+ :permissions => 0644,
603
+
604
+ # path to the unix "file" executbale for
605
+ # guessing the content-type of files
606
+ :file_exec => "file"
607
+ }
608
+
609
+ # handle the +attr+ attribute as a "file-upload" column, generating additional methods as explained
610
+ # above. You should pass the attribute's name as a symbol, like this:
611
+ #
612
+ # file_column :image
613
+ #
614
+ # You can pass in an options hash that overrides the options
615
+ # in +DEFAULT_OPTIONS+.
616
+ def file_column(attr, options={})
617
+ options = DEFAULT_OPTIONS.merge(options) if options
618
+
619
+ my_options = FileColumn::init_options(options,
620
+ ActiveSupport::Inflector.underscore(self.name).to_s,
621
+ attr.to_s)
622
+
623
+ state_attr = "@#{attr}_state".to_sym
624
+ state_method = "#{attr}_state".to_sym
625
+
626
+ define_method state_method do
627
+ result = instance_variable_get state_attr
628
+ if result.nil?
629
+ result = FileColumn::create_state(self, attr.to_s)
630
+ instance_variable_set state_attr, result
631
+ end
632
+ result
633
+ end
634
+
635
+ private state_method
636
+
637
+ define_method attr do |*args|
638
+ send(state_method).absolute_path *args
639
+ end
640
+
641
+ define_method "#{attr}_relative_path" do |*args|
642
+ send(state_method).relative_path *args
643
+ end
644
+
645
+ define_method "#{attr}_dir" do
646
+ send(state_method).absolute_dir
647
+ end
648
+
649
+ define_method "#{attr}_relative_dir" do
650
+ send(state_method).relative_dir
651
+ end
652
+
653
+ define_method "#{attr}=" do |file|
654
+ state = send(state_method).assign(file)
655
+ instance_variable_set state_attr, state
656
+ if state.options[:after_upload] and state.just_uploaded?
657
+ state.options[:after_upload].each do |sym|
658
+ self.send sym
659
+ end
660
+ end
661
+ end
662
+
663
+ define_method "#{attr}_temp" do
664
+ send(state_method).temp_path
665
+ end
666
+
667
+ define_method "#{attr}_temp=" do |temp_path|
668
+ instance_variable_set state_attr, send(state_method).assign_temp(temp_path)
669
+ end
670
+
671
+ after_save_method = "#{attr}_after_save".to_sym
672
+
673
+ define_method after_save_method do
674
+ instance_variable_set state_attr, send(state_method).after_save
675
+ end
676
+
677
+ after_save after_save_method
678
+
679
+ after_destroy_method = "#{attr}_after_destroy".to_sym
680
+
681
+ define_method after_destroy_method do
682
+ send(state_method).after_destroy
683
+ end
684
+ after_destroy after_destroy_method
685
+
686
+ define_method "#{attr}_just_uploaded?" do
687
+ send(state_method).just_uploaded?
688
+ end
689
+
690
+ # this creates a closure keeping a reference to my_options
691
+ # right now that's the only way we store the options. We
692
+ # might use a class attribute as well
693
+ define_method "#{attr}_options" do
694
+ my_options
695
+ end
696
+
697
+ private after_save_method, after_destroy_method
698
+
699
+ FileColumn::MagickExtension::file_column(self, attr, my_options) if options[:magick]
700
+ end
701
+
702
+ end
703
+
704
+ private
705
+
706
+ def self.generate_temp_name
707
+ now = Time.now
708
+ "#{now.to_i}.#{now.usec}.#{Process.pid}"
709
+ end
710
+
711
+ def self.sanitize_filename(filename)
712
+ filename = File.basename(filename.gsub("\\", "/")) # work-around for IE
713
+ filename.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
714
+ filename = "_#{filename}" if filename =~ /^\.+$/
715
+ filename = "unnamed" if filename.size == 0
716
+ filename
717
+ end
718
+
719
+ end
720
+
721
+