avatar 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 (74) hide show
  1. data/.gitignore +11 -0
  2. data/History.txt +14 -0
  3. data/License.txt +20 -0
  4. data/Manifest.txt +73 -0
  5. data/README.txt +65 -0
  6. data/Rakefile +10 -0
  7. data/config/hoe.rb +70 -0
  8. data/config/requirements.rb +17 -0
  9. data/lib/avatar.rb +44 -0
  10. data/lib/avatar/source.rb +4 -0
  11. data/lib/avatar/source/abstract_source.rb +15 -0
  12. data/lib/avatar/source/file_column_source.rb +35 -0
  13. data/lib/avatar/source/gravatar_source.rb +78 -0
  14. data/lib/avatar/source/nil_source.rb +17 -0
  15. data/lib/avatar/source/source_chain.rb +51 -0
  16. data/lib/avatar/source/static_url_source.rb +25 -0
  17. data/lib/avatar/source/string_substitution_source.rb +48 -0
  18. data/lib/avatar/version.rb +9 -0
  19. data/lib/avatar/view.rb +4 -0
  20. data/lib/avatar/view/abstract_view_support.rb +15 -0
  21. data/lib/avatar/view/action_view_support.rb +23 -0
  22. data/script/destroy +14 -0
  23. data/script/generate +14 -0
  24. data/script/txt2html +74 -0
  25. data/setup.rb +1585 -0
  26. data/tasks/deployment.rake +34 -0
  27. data/tasks/environment.rake +7 -0
  28. data/tasks/testing.rake +37 -0
  29. data/tasks/website.rake +17 -0
  30. data/test/.gitignore +3 -0
  31. data/test/lib/database.rb +2 -0
  32. data/test/lib/database.yml +10 -0
  33. data/test/lib/file_column/CHANGELOG +69 -0
  34. data/test/lib/file_column/README +54 -0
  35. data/test/lib/file_column/Rakefile +36 -0
  36. data/test/lib/file_column/TODO +6 -0
  37. data/test/lib/file_column/init.rb +13 -0
  38. data/test/lib/file_column/lib/file_column.rb +720 -0
  39. data/test/lib/file_column/lib/file_column_helper.rb +150 -0
  40. data/test/lib/file_column/lib/file_compat.rb +28 -0
  41. data/test/lib/file_column/lib/magick_file_column.rb +260 -0
  42. data/test/lib/file_column/lib/rails_file_column.rb +19 -0
  43. data/test/lib/file_column/lib/test_case.rb +124 -0
  44. data/test/lib/file_column/lib/validations.rb +112 -0
  45. data/test/lib/file_column/test/abstract_unit.rb +63 -0
  46. data/test/lib/file_column/test/connection.rb +17 -0
  47. data/test/lib/file_column/test/file_column_helper_test.rb +97 -0
  48. data/test/lib/file_column/test/file_column_test.rb +650 -0
  49. data/test/lib/file_column/test/fixtures/entry.rb +32 -0
  50. data/test/lib/file_column/test/fixtures/invalid-image.jpg +1 -0
  51. data/test/lib/file_column/test/fixtures/kerb.jpg +0 -0
  52. data/test/lib/file_column/test/fixtures/mysql.sql +25 -0
  53. data/test/lib/file_column/test/fixtures/schema.rb +10 -0
  54. data/test/lib/file_column/test/fixtures/skanthak.png +0 -0
  55. data/test/lib/file_column/test/magick_test.rb +380 -0
  56. data/test/lib/file_column/test/magick_view_only_test.rb +21 -0
  57. data/test/lib/schema.rb +7 -0
  58. data/test/lib/user_suit.png +0 -0
  59. data/test/test_abstract_view_support.rb +22 -0
  60. data/test/test_action_view_support.rb +30 -0
  61. data/test/test_avatar.rb +12 -0
  62. data/test/test_file_column_source.rb +30 -0
  63. data/test/test_gravatar_source.rb +58 -0
  64. data/test/test_helper.rb +22 -0
  65. data/test/test_nil_source.rb +18 -0
  66. data/test/test_source_chain.rb +44 -0
  67. data/test/test_static_url_source.rb +18 -0
  68. data/test/test_string_substitution_source.rb +22 -0
  69. data/website/index.html +101 -0
  70. data/website/index.txt +44 -0
  71. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  72. data/website/stylesheets/screen.css +138 -0
  73. data/website/template.rhtml +48 -0
  74. metadata +141 -0
@@ -0,0 +1,34 @@
1
+ desc 'Release the website and new gem version'
2
+ task :deploy => [:check_version, :website, :release] do
3
+ puts "Remember to create SVN tag:"
4
+ puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
5
+ "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
6
+ puts "Suggested comment:"
7
+ puts "Tagging release #{CHANGES}"
8
+ end
9
+
10
+ desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
11
+ task :local_deploy => [:website_generate, :install_gem]
12
+
13
+ task :check_version do
14
+ unless ENV['VERSION']
15
+ puts 'Must pass a VERSION=x.y.z release version'
16
+ exit
17
+ end
18
+ unless ENV['VERSION'] == VERS
19
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
20
+ exit
21
+ end
22
+ end
23
+
24
+ desc 'Install the package as a gem, without generating documentation(ri/rdoc)'
25
+ task :install_gem_no_doc => [:clean, :package] do
26
+ sh "#{'sudo ' unless Hoe::WINDOZE }gem install pkg/*.gem --no-rdoc --no-ri"
27
+ end
28
+
29
+ namespace :manifest do
30
+ desc 'Recreate Manifest.txt to include ALL files'
31
+ task :refresh do
32
+ `rake check_manifest | patch -p0 > Manifest.txt`
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ task :ruby_env do
2
+ RUBY_APP = if RUBY_PLATFORM =~ /java/
3
+ "jruby"
4
+ else
5
+ "ruby"
6
+ end unless defined? RUBY_APP
7
+ end
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
6
+
7
+ LIB_DIRECTORIES = FileList.new do |fl|
8
+ fl.include "#{PROJECT_ROOT}/lib"
9
+ fl.include "#{PROJECT_ROOT}/test/lib/file_column/lib"
10
+ end
11
+
12
+ TEST_FILES = FileList.new do |fl|
13
+ fl.include "#{PROJECT_ROOT}/test/**/test_*.rb"
14
+ fl.exclude "#{PROJECT_ROOT}/test/test_helper.rb"
15
+ fl.exclude "#{PROJECT_ROOT}/test/lib/**/*.rb"
16
+ end
17
+
18
+ Rake.application.remove_task :test
19
+
20
+ desc 'Run all tests'
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs = LIB_DIRECTORIES
23
+ t.test_files = TEST_FILES
24
+ t.verbose = true
25
+ end
26
+
27
+ desc "Build a code coverage report"
28
+ task :coverage do
29
+ files = TEST_FILES.join(" ")
30
+ sh "rcov -o coverage #{files} --exclude ^/Library/Ruby/,^init.rb --include lib/ --include-file ^lib/.*\\.rb"
31
+ end
32
+
33
+ namespace :coverage do
34
+ task :clean do
35
+ rm_r 'coverage' if File.directory?('coverage')
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ desc 'Generate website files'
2
+ task :website_generate => :ruby_env do
3
+ (Dir['website/**/*.txt'] - Dir['website/version*.txt']).each do |txt|
4
+ sh %{ #{RUBY_APP} script/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
5
+ end
6
+ end
7
+
8
+ desc 'Upload website files to rubyforge'
9
+ task :website_upload do
10
+ host = "#{rubyforge_username}@rubyforge.org"
11
+ remote_dir = "/var/www/gforge-projects/#{PATH}/"
12
+ local_dir = 'website'
13
+ sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
14
+ end
15
+
16
+ desc 'Generate and upload website files'
17
+ task :website => [:website_generate, :website_upload, :publish_docs]
@@ -0,0 +1,3 @@
1
+ # ignore the tmp and public directories used for file_column images storage:
2
+ /public
3
+ /tmp
@@ -0,0 +1,2 @@
1
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
2
+ ActiveRecord::Base.establish_connection(ENV['DB'] || 'sqlite3')
@@ -0,0 +1,10 @@
1
+ mysql:
2
+ :adapter: mysql
3
+ :host: localhost
4
+ :username: rails
5
+ :password:
6
+ :database: rails_plugin_test
7
+
8
+ sqlite3:
9
+ :adapter: sqlite3
10
+ :database: ':memory:'
@@ -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
@@ -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
+
@@ -0,0 +1,36 @@
1
+ task :default => [:test]
2
+
3
+ PKG_NAME = "file-column"
4
+ PKG_VERSION = "0.3.1"
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
@@ -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?)
@@ -0,0 +1,13 @@
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
+ #require 'test_case' if RAILS_ENV == 'test'
10
+
11
+ ActiveRecord::Base.send(:include, FileColumn)
12
+ ActionView::Base.send(:include, FileColumnHelper)
13
+ ActiveRecord::Base.send(:include, FileColumn::Validations)
@@ -0,0 +1,720 @@
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
+ end
353
+
354
+ def move_from(local_dir, just_uploaded)
355
+ # remove old permament dir first
356
+ # this creates a short moment, where neither the old nor
357
+ # the new files exist but we can't do much about this as
358
+ # filesystems aren't transactional.
359
+ FileUtils.rm_rf @dir
360
+
361
+ FileUtils.mv local_dir, @dir
362
+
363
+ @just_uploaded = just_uploaded
364
+ end
365
+
366
+ def upload(file)
367
+ temp = clone_as TempUploadedFile
368
+ temp.store_upload(file)
369
+ temp
370
+ end
371
+
372
+ def delete
373
+ file = clone_as NoUploadedFile
374
+ @instance[@attr] = ""
375
+ file.on_save { delete_files }
376
+ file
377
+ end
378
+
379
+ def assign_temp(temp_path)
380
+ return nil if temp_path.nil? or temp_path.empty?
381
+
382
+ temp = clone_as TempUploadedFile
383
+ temp.parse_temp_path(temp_path)
384
+ temp
385
+ end
386
+
387
+ def after_destroy
388
+ delete_files
389
+ end
390
+
391
+ def delete_files
392
+ FileUtils.rm_rf @dir
393
+ end
394
+
395
+ private
396
+
397
+ def relative_path_prefix
398
+ raise RuntimeError.new("Trying to access file_column, but primary key got lost.") if @instance.id.to_s.empty?
399
+ @instance.id.to_s
400
+ end
401
+ end
402
+
403
+ # The FileColumn module allows you to easily handle file uploads. You can designate
404
+ # one or more columns of your model's table as "file columns" like this:
405
+ #
406
+ # class Entry < ActiveRecord::Base
407
+ #
408
+ # file_column :image
409
+ # end
410
+ #
411
+ # Now, by default, an uploaded file "test.png" for an entry object with primary key 42 will
412
+ # be stored in in "public/entry/image/42/test.png". The filename "test.png" will be stored
413
+ # in the record's "image" column. The "entries" table should have a +VARCHAR+ column
414
+ # named "image".
415
+ #
416
+ # The methods of this module are automatically included into <tt>ActiveRecord::Base</tt>
417
+ # as class methods, so that you can use them in your models.
418
+ #
419
+ # == Generated Methods
420
+ #
421
+ # After calling "<tt>file_column :image</tt>" as in the example above, a number of instance methods
422
+ # will automatically be generated, all prefixed by "image":
423
+ #
424
+ # * <tt>Entry#image=(uploaded_file)</tt>: this will handle a newly uploaded file
425
+ # (see below). Note that
426
+ # you can simply call your upload field "entry[image]" in your view (or use the
427
+ # helper).
428
+ # * <tt>Entry#image(subdir=nil)</tt>: This will return an absolute path (as a
429
+ # string) to the currently uploaded file
430
+ # or nil if no file has been uploaded
431
+ # * <tt>Entry#image_relative_path(subdir=nil)</tt>: This will return a path relative to
432
+ # this file column's base directory
433
+ # as a string or nil if no file has been uploaded. This would be "42/test.png" in the example.
434
+ # * <tt>Entry#image_just_uploaded?</tt>: Returns true if a new file has been uploaded to this instance.
435
+ # You can use this in your code to perform certain actions (e. g., validation,
436
+ # custom post-processing) only on newly uploaded files.
437
+ #
438
+ # You can access the raw value of the "image" column (which will contain the filename) via the
439
+ # <tt>ActiveRecord::Base#attributes</tt> or <tt>ActiveRecord::Base#[]</tt> methods like this:
440
+ #
441
+ # entry['image'] # e.g."test.png"
442
+ #
443
+ # == Storage of uploaded files
444
+ #
445
+ # For a model class +Entry+ and a column +image+, all files will be stored under
446
+ # "public/entry/image". A sub-directory named after the primary key of the object will
447
+ # be created, so that files can be stored using their real filename. For example, a file
448
+ # "test.png" stored in an Entry object with id 42 will be stored in
449
+ #
450
+ # public/entry/image/42/test.png
451
+ #
452
+ # Files will be moved to this location in an +after_save+ callback. They will be stored in
453
+ # a temporary location previously as explained in the next section.
454
+ #
455
+ # By default, files will be created with unix permissions of <tt>0644</tt> (i. e., owner has
456
+ # read/write access, group and others only have read access). You can customize
457
+ # this by passing the desired mode as a <tt>:permissions</tt> options. The value
458
+ # you give here is passed directly to <tt>File::chmod</tt>, so on Unix you should
459
+ # give some octal value like 0644, for example.
460
+ #
461
+ # == Handling of form redisplay
462
+ #
463
+ # Suppose you have a form for creating a new object where the user can upload an image. The form may
464
+ # have to be re-displayed because of validation errors. The uploaded file has to be stored somewhere so
465
+ # that the user does not have to upload it again. FileColumn will store these in a temporary directory
466
+ # (called "tmp" and located under the column's base directory by default) so that it can be moved to
467
+ # the final location if the object is successfully created. If the form is never completed, though, you
468
+ # can easily remove all the images in this "tmp" directory once per day or so.
469
+ #
470
+ # So in the example above, the image "test.png" would first be stored in
471
+ # "public/entry/image/tmp/<some_random_key>/test.png" and be moved to
472
+ # "public/entry/image/<primary_key>/test.png".
473
+ #
474
+ # This temporary location of newly uploaded files has another advantage when updating objects. If the
475
+ # update fails for some reasons (e.g. due to validations), the existing image will not be overwritten, so
476
+ # it has a kind of "transactional behaviour".
477
+ #
478
+ # == Additional Files and Directories
479
+ #
480
+ # FileColumn allows you to keep more than one file in a directory and will move/delete
481
+ # all the files and directories it finds in a model object's directory when necessary.
482
+ #
483
+ # As a convenience you can access files stored in sub-directories via the +subdir+
484
+ # parameter if they have the same filename.
485
+ #
486
+ # Suppose your uploaded file is named "vancouver.jpg" and you want to create a
487
+ # thumb-nail and store it in the "thumb" directory. If you call
488
+ # <tt>image("thumb")</tt>, you
489
+ # will receive an absolute path for the file "thumb/vancouver.jpg" in the same
490
+ # directory "vancouver.jpg" is stored. Look at the documentation of FileColumn::Magick
491
+ # for more examples and how to create these thumb-nails automatically.
492
+ #
493
+ # == File Extensions
494
+ #
495
+ # FileColumn will try to fix the file extension of uploaded files, so that
496
+ # the files are served with the correct mime-type by your web-server. Most
497
+ # web-servers are setting the mime-type based on the file's extension. You
498
+ # can disable this behaviour by passing the <tt>:fix_file_extensions</tt> option
499
+ # with a value of +nil+ to +file_column+.
500
+ #
501
+ # In order to set the correct extension, FileColumn tries to determine
502
+ # the files mime-type first. It then uses the +MIME_EXTENSIONS+ hash to
503
+ # choose the corresponding file extension. You can override this hash
504
+ # by passing in a <tt>:mime_extensions</tt> option to +file_column+.
505
+ #
506
+ # The mime-type of the uploaded file is determined with the following steps:
507
+ #
508
+ # 1. Run the external "file" utility. You can specify the full path to
509
+ # the executable in the <tt>:file_exec</tt> option or set this option
510
+ # to +nil+ to disable this step
511
+ #
512
+ # 2. If the file utility couldn't determine the mime-type or the utility was not
513
+ # present, the content-type provided by the user's browser is used
514
+ # as a fallback.
515
+ #
516
+ # == Custom Storage Directories
517
+ #
518
+ # FileColumn's storage location is determined in the following way. All
519
+ # files are saved below the so-called "root_path" directory, which defaults to
520
+ # "RAILS_ROOT/public". For every file_column, you can set a separte "store_dir"
521
+ # option. It defaults to "model_name/attribute_name".
522
+ #
523
+ # Files will always be stored in sub-directories of the store_dir path. The
524
+ # subdirectory is named after the instance's +id+ attribute for a saved model,
525
+ # or "tmp/<randomkey>" for unsaved models.
526
+ #
527
+ # You can specify a custom root_path by setting the <tt>:root_path</tt> option.
528
+ #
529
+ # You can specify a custom storage_dir by setting the <tt>:storage_dir</tt> option.
530
+ #
531
+ # For setting a static storage_dir that doesn't change with respect to a particular
532
+ # instance, you assign <tt>:storage_dir</tt> a String representing a directory
533
+ # as an absolute path.
534
+ #
535
+ # If you need more fine-grained control over the storage directory, you
536
+ # can use the name of a callback-method as a symbol for the
537
+ # <tt>:store_dir</tt> option. This method has to be defined as an
538
+ # instance method in your model. It will be called without any arguments
539
+ # whenever the storage directory for an uploaded file is needed. It should return
540
+ # a String representing a directory relativeo to root_path.
541
+ #
542
+ # Uploaded files for unsaved models objects will be stored in a temporary
543
+ # directory. By default this directory will be a "tmp" directory in
544
+ # your <tt>:store_dir</tt>. You can override this via the
545
+ # <tt>:tmp_base_dir</tt> option.
546
+ module ClassMethods
547
+
548
+ # default mapping of mime-types to file extensions. FileColumn will try to
549
+ # rename a file to the correct extension if it detects a known mime-type
550
+ MIME_EXTENSIONS = {
551
+ "image/gif" => "gif",
552
+ "image/jpeg" => "jpg",
553
+ "image/pjpeg" => "jpg",
554
+ "image/x-png" => "png",
555
+ "image/jpg" => "jpg",
556
+ "image/png" => "png",
557
+ "application/x-shockwave-flash" => "swf",
558
+ "application/pdf" => "pdf",
559
+ "application/pgp-signature" => "sig",
560
+ "application/futuresplash" => "spl",
561
+ "application/msword" => "doc",
562
+ "application/postscript" => "ps",
563
+ "application/x-bittorrent" => "torrent",
564
+ "application/x-dvi" => "dvi",
565
+ "application/x-gzip" => "gz",
566
+ "application/x-ns-proxy-autoconfig" => "pac",
567
+ "application/x-shockwave-flash" => "swf",
568
+ "application/x-tgz" => "tar.gz",
569
+ "application/x-tar" => "tar",
570
+ "application/zip" => "zip",
571
+ "audio/mpeg" => "mp3",
572
+ "audio/x-mpegurl" => "m3u",
573
+ "audio/x-ms-wma" => "wma",
574
+ "audio/x-ms-wax" => "wax",
575
+ "audio/x-wav" => "wav",
576
+ "image/x-xbitmap" => "xbm",
577
+ "image/x-xpixmap" => "xpm",
578
+ "image/x-xwindowdump" => "xwd",
579
+ "text/css" => "css",
580
+ "text/html" => "html",
581
+ "text/javascript" => "js",
582
+ "text/plain" => "txt",
583
+ "text/xml" => "xml",
584
+ "video/mpeg" => "mpeg",
585
+ "video/quicktime" => "mov",
586
+ "video/x-msvideo" => "avi",
587
+ "video/x-ms-asf" => "asf",
588
+ "video/x-ms-wmv" => "wmv"
589
+ }
590
+
591
+ EXTENSIONS = Set.new MIME_EXTENSIONS.values
592
+ EXTENSIONS.merge %w(jpeg)
593
+
594
+ # default options. You can override these with +file_column+'s +options+ parameter
595
+ DEFAULT_OPTIONS = {
596
+ :root_path => File.join(RAILS_ROOT, "public"),
597
+ :web_root => "",
598
+ :mime_extensions => MIME_EXTENSIONS,
599
+ :extensions => EXTENSIONS,
600
+ :fix_file_extensions => true,
601
+ :permissions => 0644,
602
+
603
+ # path to the unix "file" executbale for
604
+ # guessing the content-type of files
605
+ :file_exec => "file"
606
+ }
607
+
608
+ # handle the +attr+ attribute as a "file-upload" column, generating additional methods as explained
609
+ # above. You should pass the attribute's name as a symbol, like this:
610
+ #
611
+ # file_column :image
612
+ #
613
+ # You can pass in an options hash that overrides the options
614
+ # in +DEFAULT_OPTIONS+.
615
+ def file_column(attr, options={})
616
+ options = DEFAULT_OPTIONS.merge(options) if options
617
+
618
+ my_options = FileColumn::init_options(options,
619
+ Inflector.underscore(self.name).to_s,
620
+ attr.to_s)
621
+
622
+ state_attr = "@#{attr}_state".to_sym
623
+ state_method = "#{attr}_state".to_sym
624
+
625
+ define_method state_method do
626
+ result = instance_variable_get state_attr
627
+ if result.nil?
628
+ result = FileColumn::create_state(self, attr.to_s)
629
+ instance_variable_set state_attr, result
630
+ end
631
+ result
632
+ end
633
+
634
+ private state_method
635
+
636
+ define_method attr do |*args|
637
+ send(state_method).absolute_path *args
638
+ end
639
+
640
+ define_method "#{attr}_relative_path" do |*args|
641
+ send(state_method).relative_path *args
642
+ end
643
+
644
+ define_method "#{attr}_dir" do
645
+ send(state_method).absolute_dir
646
+ end
647
+
648
+ define_method "#{attr}_relative_dir" do
649
+ send(state_method).relative_dir
650
+ end
651
+
652
+ define_method "#{attr}=" do |file|
653
+ state = send(state_method).assign(file)
654
+ instance_variable_set state_attr, state
655
+ if state.options[:after_upload] and state.just_uploaded?
656
+ state.options[:after_upload].each do |sym|
657
+ self.send sym
658
+ end
659
+ end
660
+ end
661
+
662
+ define_method "#{attr}_temp" do
663
+ send(state_method).temp_path
664
+ end
665
+
666
+ define_method "#{attr}_temp=" do |temp_path|
667
+ instance_variable_set state_attr, send(state_method).assign_temp(temp_path)
668
+ end
669
+
670
+ after_save_method = "#{attr}_after_save".to_sym
671
+
672
+ define_method after_save_method do
673
+ instance_variable_set state_attr, send(state_method).after_save
674
+ end
675
+
676
+ after_save after_save_method
677
+
678
+ after_destroy_method = "#{attr}_after_destroy".to_sym
679
+
680
+ define_method after_destroy_method do
681
+ send(state_method).after_destroy
682
+ end
683
+ after_destroy after_destroy_method
684
+
685
+ define_method "#{attr}_just_uploaded?" do
686
+ send(state_method).just_uploaded?
687
+ end
688
+
689
+ # this creates a closure keeping a reference to my_options
690
+ # right now that's the only way we store the options. We
691
+ # might use a class attribute as well
692
+ define_method "#{attr}_options" do
693
+ my_options
694
+ end
695
+
696
+ private after_save_method, after_destroy_method
697
+
698
+ FileColumn::MagickExtension::file_column(self, attr, my_options) if options[:magick]
699
+ end
700
+
701
+ end
702
+
703
+ private
704
+
705
+ def self.generate_temp_name
706
+ now = Time.now
707
+ "#{now.to_i}.#{now.usec}.#{Process.pid}"
708
+ end
709
+
710
+ def self.sanitize_filename(filename)
711
+ filename = File.basename(filename.gsub("\\", "/")) # work-around for IE
712
+ filename.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
713
+ filename = "_#{filename}" if filename =~ /^\.+$/
714
+ filename = "unnamed" if filename.size == 0
715
+ filename
716
+ end
717
+
718
+ end
719
+
720
+