file_column_with_s3 0.1.0

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