├── .gitignore ├── CHANGELOG ├── LICENSE ├── README ├── Rakefile ├── init.rb ├── lib ├── upload_column.rb └── upload_column │ ├── active_record_extension.rb │ ├── configuration.rb │ ├── magic_columns.rb │ ├── manipulators │ ├── image_science.rb │ └── rmagick.rb │ ├── rails │ ├── action_controller_extension.rb │ ├── asset_tag_extension.rb │ └── upload_column_helper.rb │ ├── sanitized_file.rb │ └── uploaded_file.rb └── spec ├── active_record_extension_spec.rb ├── custom_matchers.rb ├── fixtures ├── animated.gif ├── animated_solarized.gif ├── invalid-image.jpg ├── kerb.jpg ├── kerb_solarized.jpg ├── netscape.gif └── skanthak.png ├── image_science_manipulator_spec.rb ├── integration_spec.rb ├── magic_columns_spec.rb ├── rmagick_manipulator_spec.rb ├── sanitized_file_spec.rb ├── spec_helper.rb ├── upload_column_spec.rb └── uploaded_file_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | spec/public 3 | spec/db -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.3 2 | 3 | Note: The 0.3 branch is a complete rewrite of everything, with a similar (but not identical) API, don't expect it to work drop-in. 4 | 5 | [TODO] You can now set a default image if the column is blank 6 | 7 | [TODO] Integrated mocking support, to make testing your UploadColumns easier. 8 | 9 | [NEW] Complete rewrite, maintaining most of the legacy API 10 | 11 | [NEW] Framework for custom manipulators, for clean and easy extension. 12 | 13 | [NEW] Manipulate images with ImageScience instead of RMagick if you want 14 | 15 | [NEW] Support for animated GIFs in the RMagick manipulator 16 | 17 | [NEW] Fully tested with RSpec 18 | 19 | [NEW] Following the law of Demeter better by adding _public_path, _thumb, _thumb_public_path etc... magic methods. 20 | 21 | [DEPRECATED] UploadedFile#url is deprecated in favour of UploadedFile#public_path and will be removed in the next major release 22 | 23 | [CHANGED] Store dir and temp dir procs now take two piped variables, the UploadedFile object and the ActiveRecord 24 | 25 | [CHANGED] the :root_path option is now called :root_dir 26 | 27 | [CHANGED] web_root must now be set with a leading slash if an absolute URL is desired and no trailing slash. 28 | 29 | [CHANGED] UploadedFile#filename_base is removed in favor of #basename 30 | 31 | [CHANGED] UploadedFile#filename_extension is removed in favor of #extension 32 | 33 | [CHANGED] UploadedFile#mime_type is removed in favor of #content_type 34 | 35 | [CHANGED] _store_dir and _tmp_dir callbacks now take the file object as an argument, which means that the methods always MUST take an argument, even if you don't need it. 36 | 37 | [CHANGED] The _after_assigns callback has changed name to _after_upload and take the UploadedFile as param. 38 | 39 | [REMOVED] Support for the exif columns, as in 0.2.X has been removed temporarily. It might be readded later. 40 | 41 | [REMOVED] The old_files option as in 0.2.X has been removed. All old files are now stored. This option will be reintroduced in later versions of 0.3.X 42 | 43 | [REMOVED] The :force_format option for image_columns as in 0.2.X has been removed and will be reintroduced later. 44 | 45 | [REMOVED] The remote_upload_form helper has been removed, for encouraging bad JS practice. It will not be readded. If you need remote uploads, uploading with Flash is much cooler, and can be done in an unobtrusive way. Check out Swiff.js for hints. 46 | 47 | [REMOVED] The 'image' helper has been removed, since its functionality is not really useful in any app (hopefully most apps) that uses named routes. 48 | 49 | ============================ 50 | 51 | 0.2.1 52 | 53 | * Added :force_format option to image_column 54 | 55 | * Various Rails 1.2.1 compatibility fixes (mainly in test) 56 | 57 | * Added :permissions option to upload_column. 58 | 59 | * You can now assign normal Ruby File objects to upload and image columns 60 | 61 | * upload_form_tag and remote_upload_form_tag now accept a block, just like Rails' form_tag 62 | 63 | * You can now pass :none to image_column versions so nothing will be done to your image. 64 | 65 | * FIXED #8109 Parallell uploads no longer wipe each other out 66 | 67 | * WARNING: Compatibility with Rails < 1.2.1 dropped 68 | 69 | ============================ 70 | 71 | 0.2 72 | 73 | * A freaking huge refactoring of the code, basically ALL of the methods for accessing paths have changed, except for path itself. This was overdue and I apologize if it breaks anything, but I felt that the gain in consistency was worth it. It now works like this: 74 | 75 | path --the current path of the file (including the filename) 76 | relative_path --the current path of the file relative to the root_path option 77 | 78 | dir --the directory where the file is currently stored 79 | relative_dir --like dir but relative to root_path 80 | 81 | store_dir --The directory where files are permanently stored 82 | relative_store_dir --the same but relative to root_dir 83 | 84 | tmp_dir --The directory where tempfiles are stored 85 | relative_tmp_dir --you can work this out yourself 86 | 87 | As you can see, this is now actually consistent, with all the relative paths relative to the same directory (err... wow?) and a consistent naming convention. 88 | 89 | * In related news: you can now pass a Proc to the :store_dir and :tmp_dir options. The default options are now also procs, instead of being some kind of arcane super-exception like before. The procs will be passed to arguments, first the current model instance and the name of the upload column as the second. 90 | 91 | * The :accumulate option was removed from :old_files. I really liked it, but it doesn't make sense with th new Proc-based system (it would wipe out data without thinking, thus potentially getting rid of files you want to keep). Use :keep instead or implement some kind of versioning. The new default is :delete. So beware, if you need to keep those files, make sure to change it! 92 | 93 | * You can now specify individual versions that should be cropped in image_columns, simply add a 'c' before the string that specifies the size, so you can do: 94 | 95 | image_column :picture, :versions => { :thumb => "100x100", :banner => "c400x200" } 96 | 97 | Where thumb will be no larger than 100x100 (but might be smaller) and banner will be cropped to 400x200 exactly! 98 | 99 | * Furthermore you can pass a Proc instead of a string to an image_column version: 100 | 101 | image_column :picture, :versions => { :thumb => "100x100", :solarized => proc{|img| img.solarize} } 102 | 103 | The Proc will be passed an RMagick object, just like process! 104 | 105 | * render_image now uses send_file if no block is given for faster performance. 106 | 107 | * FIXED #6955 store_dir callback called when the file is assigned 108 | 109 | * FIXED #7697 Editing with old-files :delete / :replace erases the original file 110 | 111 | * FIXED #7686 Problem uploading files with spaces in name 112 | 113 | ============================ 114 | 115 | 1.1.2 (unreleased) 116 | 117 | * new :validates_integrity option replaces the old validates_integrity_of. The latter was more elegant, but posed a security risk, since files would be stored on the server in a remotely accessible location without having been validated. I tried to fix the bug, but couldn't make it work, so I opted for the less elegant, but safe solution instead. 118 | 119 | * readded the :file_exec option, it's now possible to set this manually again. I cut it originally, because I felt that it was unneccessary and that there were too many options already, I readded it mainly to make it possible to test the validation better. 120 | 121 | * assign, save, delete_temporary_files, delete, filename= and dir= are now all private, I see no reason why they should be public, and since they aren't really useful out of context I think it makes for a cleaner API to make them private, if you still need to use them, you can use .send(:save), etc.. instead. 122 | 123 | * Added magic columns, see the readme for detaills. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Sebastian Kanthak, Jonas Nicklas 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = UploadColumn 2 | 3 | UploadColumn is a plugin for the Ruby on Rails framework that enables easy uploading of files, especially images. 4 | 5 | Suppose you have a list of users, and you would like to associate a picture to each of them. You could upload the image to a database, or you could use upload_column for simple storage to the file system. 6 | 7 | Assuming you have a User model with a column called 'picture' that is of type String, you could simply add the upload_column instruction to your User model: 8 | 9 | class User < ActiveRecord::Base 10 | upload_column :picture 11 | end 12 | 13 | That's it! You can start uploading files. Of course, +upload_column+ has a lot of different options you can use to customize your uploads. 14 | 15 | Uploading files is no fun without a user interface, so get going and make one: 16 | 17 | add an upload_column_field to your form, maybe like this: 18 | 19 |


20 | <%= upload_column_field 'user', 'picture' %>

21 | 22 | You should use upload_column_field instead of Rails' file_field, since it will work even when the form is redisplayed, like when a validation fails. Unfortunately file_field doesn't work in that case. 23 | 24 | Now that's excellent, but most likely it will fail, because instead of sending the file, it just sends a string. No worries though, if we just set the form's encoding to multipart it will all work out, UploadColumn even comes with some nice helpers to avoid that nasty multipart syntax. This could look something like this: 25 | 26 | <%= upload_form_tag( :action => 'create' ) %> 27 | 28 | And that's it! Your uploads are up and running (hopefully) and you should now be able to add pictures to your users. The madness doesn't stop there of course! 29 | 30 | == Storage Path 31 | 32 | You won't always want to store the pictures in the directory that upload_column selects for you, but that's not a problem, because changing that directory is trivial. You can pass a :store_dir key to the upload_column declaration, this will override the default mechanism and always use that directory as the basis. 33 | 34 | upload_column :picture, :store_dir => "pictures" 35 | 36 | might be sensible in our case. Note that this way, all files will be stored in the same directory. 37 | 38 | If you need more refined control over the storage path (maybe you need to store it by the id of an association?) then you can use a proc instead. Our proc might look like this: 39 | 40 | upload_column :picture, :store_dir => proc{|record, file| "images/#{record.category.name}/#{record.id}/#{file.extension}"} 41 | 42 | The proc will be passed two parameters, the first is the current instance of your model class, the second is the name of the attribute that is being uploaded to (in our case +attr+ would be :picture). 43 | 44 | You can change the :tmp_dir in the same way. 45 | 46 | == Filename 47 | 48 | By default, UploadColumn will keep the name of the original file, however this might be inconvenient in some cases. You can pass a :filename directive to your upload_column declaration: 49 | 50 | upload_column :picture, :filename => "donkey.png" 51 | 52 | In which case all files will be named +donkey.png+. This is not desirable if the file in question is a jpeg file of course. Usually it is more sensible to pass a Proc to :filename. 53 | 54 | upload_column :picture, :filename => proc{|record, file| "avatar#{record.id}.#{file.extension}"} 55 | 56 | The Proc will be passed two parameters, the current instance, and the file itself. 57 | 58 | == Manipulators 59 | 60 | UploadColumn allows you to use manipulators on your file, that in some way transform your file, or perform any kind 61 | of operations on it. There are currently two manipulators bundled, the RMagick manipulator and the ImageScience 62 | manipulator, but writing your own is very easy. There are further instructions on the website. 63 | 64 | == Manipulating Images with RMagick 65 | 66 | Say you would want (for whatever reason) to have a funky solarize effect on your users' images. Manipulating images with upload_column can be done either at runtime or after the image is saved, let's look at some possibilities: 67 | 68 | class User < ActiveRecord::Base 69 | upload_column :picture, :manipulator => UploadColumn::Manipulators::RMagick 70 | 71 | def picture_after_assign 72 | 73 | picture.process! do |img| 74 | img.solarize 75 | end 76 | 77 | end 78 | end 79 | 80 | You can also use the :process instruction, which will automatically apply the manipulation when a new image is uploaded. If you wanted to resize your image to a maximum of 800 by 600 pixels for example, you could do: 81 | 82 | class User < ActiveRecord::Base 83 | upload_column :picture, :process => '800x600', :manipulator => UploadColumn::Manipulators::RMagick 84 | end 85 | 86 | the previous example with solarize could be written shorter as: 87 | 88 | class User < ActiveRecord::Base 89 | upload_column :picture, :process => proc{|img| img.solarize }, :manipulator => UploadColumn::Manipulators::RMagick 90 | end 91 | 92 | Or maybe we want different versions of our image, then we could simply specify: 93 | 94 | class User < ActiveRecord::Base 95 | upload_column :picture, :versions => [ :solarized, :sepiatoned ], :manipulator => UploadColumn::Manipulators::RMagick 96 | 97 | def picture_after_assign 98 | picture.solarized.process! do |img| 99 | img.solarize 100 | end 101 | picture.sepiatoned.process! do |img| 102 | img.sepiatone 103 | end 104 | end 105 | end 106 | 107 | you can also use a Hash for versions and pass a dimension or a proc to it, so you can do: 108 | 109 | class User < ActiveRecord::Base 110 | upload_column :picture, :versions => { :thumb => "c100x100", :large => "200x300", :sepiatoned => proc{ |img| img.sepiatone } }, :manipulator => UploadColumn::Manipulators::RMagick 111 | end 112 | 113 | Note the 'c' in front of the dimensions for the thumb image, this will crop the image to the exact dimensions. All of this is a bit wordy though, and it also doesn't take check, that the files really are images. Sepiatoning the latest GreenDay song somehow doesn't sound too good. For that reason UploadColumn comes with the image_column function: 114 | 115 | class User < ActiveRecord::Base 116 | image_column :picture, :versions => { :thumb => "c100x100", :large => "200x300", :sepiatoned => proc{ |img| img.sepiatone } } 117 | end 118 | 119 | This also puts your images in public/images instead of public, which is neat! 120 | 121 | == Runtime rendering 122 | 123 | You can manipulate images at runtime (it's a huge performance hit though!). In your controller add an action and use UploadColumnRenderHelper.render_image. 124 | 125 | def sepiatone 126 | @user = User.find(parms[:id]) 127 | render_image @user.picture do |img| 128 | img.sepiatone 129 | end 130 | end 131 | 132 | And that's it! 133 | 134 | In your view, you can use UploadColumnHelper.image to easily create an image tag for your action: 135 | 136 | <%= image :action => "sepiatone", :id => 5 %> 137 | 138 | == Views 139 | 140 | If your uploaded file is an image you would most likely want to display it in your view, if it's another kind of file you'll want to link to it. Both of these are easy using UploadColumn::BaseUploadedFile.url. 141 | 142 | <%= link_to "Guitar Tablature", @song.tab.url %> 143 | 144 | <%= image_tag @user.picture.url %> 145 | 146 | == Magic Columns 147 | 148 | UploadColumn allows you to add 'magic' columns to your model, which will be automatically filled with the appropriate data. Just add the column, for example via migrations: 149 | 150 | add_column :users, :picture_content_type 151 | 152 | And if our model looks like this: 153 | 154 | class User < ActiveRecord::Base 155 | upload_column :picture 156 | end 157 | 158 | The column picture_content_type will now automatically be filled with the file's content-type (or at least with UploadColumn's best guess ;). 159 | 160 | You can use any method method on UploadColumn::UploadedFile that takes no argument, so you can use for example, size, url, store_dir and so on. 161 | 162 | You can also do picture_exif_date_time or picture_exif_model, etc. This works only, of course, if the uploaded file is a JPEG image, since that is the only filetype that has exif data. This requires the EXIFR library, which you can get by installing the gem via gem install exifr. 163 | 164 | == Validations 165 | 166 | UploadColumn comes with its own validation method, validates_integrity_of. This method will ensure that only files with an extension from a whitelist will be uploaded. This prevents a hacker from uploading executable files (such as .rb, .pl or .cgi for example) or it can be used to restrict what kind of file are allowed to be uploaded, for example only images. You can customize the whitelist with the :extensions parameter to upload column. 167 | 168 | If you want to only allow the upload of XHTML and XML files, so you can manipulate them with XSLT you could do: 169 | 170 | upload_column :xml, :extensions => %w(xml html htm), :manipulator => MyXSLTProcessor 171 | 172 | validate_integrity_of :xml 173 | 174 | You can also use some of Rails' validations with UploadColumn. 175 | 176 | validates_presence_of and validates_size_of have been verified to work. 177 | 178 | validates_size_of :image, :maximum => 200000, :message => "is too big, must be smaller than 200kB!" 179 | 180 | Remember to change the error message, the default one sounds a bit stupid with UploadColumn. 181 | 182 | validates_uniqueness_of does NOT work, this is because validates_uniqueness_of will send(:your_upload_column) instead of asking for the instance variable, thus it will get an UploadedFile object, which it can't really compare to other values in the database, this is rather difficult to work around without messing with Rails internals (if you manage, please let me know!). Meanwhile you could do 183 | 184 | validates_each :your_upload_column do |record, attr, value| 185 | record.errors.add attr, 'already exists!' if YourModel.find( :first, :conditions => ["#{attr.to_s} = ?", value ] ) 186 | end 187 | 188 | It's not elegant I know, but it should work. 189 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | require 'spec/rake/spectask' 5 | 6 | file_list = FileList['spec/*_spec.rb'] 7 | 8 | namespace :spec do 9 | desc "Run all examples with RCov" 10 | Spec::Rake::SpecTask.new('rcov') do |t| 11 | t.spec_files = file_list 12 | t.rcov = true 13 | t.rcov_dir = "doc/coverage" 14 | t.rcov_opts = ['--exclude', 'spec'] 15 | end 16 | 17 | desc "Generate an html report" 18 | Spec::Rake::SpecTask.new('report') do |t| 19 | t.spec_files = file_list 20 | t.rcov = true 21 | t.rcov_dir = "doc/coverage" 22 | t.rcov_opts = ['--exclude', 'spec'] 23 | t.spec_opts = ["--format", "html:doc/reports/specs.html"] 24 | t.fail_on_error = false 25 | end 26 | 27 | desc "heckle all" 28 | task :heckle => [ 'spec:heckle:uploaded_file', 'spec:heckle:sanitized_file' ] 29 | 30 | namespace :heckle do 31 | desc "Heckle UploadedFile" 32 | Spec::Rake::SpecTask.new('uploaded_file') do |t| 33 | t.spec_files = [ File.join(File.dirname(__FILE__), *%w[spec uploaded_file_spec.rb]) ] 34 | t.spec_opts = ["--heckle", "UploadColumn::UploadedFile"] 35 | end 36 | 37 | desc "Heckle SanitizedFile" 38 | Spec::Rake::SpecTask.new('sanitized_file') do |t| 39 | t.spec_files = [ File.join(File.dirname(__FILE__), *%w[spec uploaded_file_spec.rb]) ] 40 | t.spec_opts = ["--heckle", "UploadColumn::SanitizedFile"] 41 | end 42 | end 43 | 44 | end 45 | 46 | 47 | desc 'Default: run unit tests.' 48 | task :default => 'spec:rcov' 49 | 50 | namespace "doc" do 51 | 52 | desc 'Generate documentation for the UploadColumn plugin.' 53 | Rake::RDocTask.new(:normal) do |rdoc| 54 | rdoc.rdoc_dir = 'doc/rdoc' 55 | rdoc.title = 'UploadColumn' 56 | rdoc.options << '--line-numbers' << '--inline-source' 57 | rdoc.rdoc_files.include('README') 58 | rdoc.rdoc_files.include('lib/**/*.rb') 59 | end 60 | 61 | desc 'Generate documentation for the UploadColumn plugin using the allison template.' 62 | Rake::RDocTask.new(:allison) do |rdoc| 63 | rdoc.rdoc_dir = 'doc/rdoc' 64 | rdoc.title = 'UploadColumn' 65 | rdoc.options << '--line-numbers' << '--inline-source' 66 | rdoc.rdoc_files.include('README') 67 | rdoc.rdoc_files.include('lib/**/*.rb') 68 | rdoc.main = "README" # page to start on 69 | rdoc.template = "~/Projects/allison2/allison/allison.rb" 70 | end 71 | end -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # plugin init file for rails 2 | # this file will be picked up by rails automatically and 3 | # add the upload_column extensions to rails 4 | 5 | require File.join(File.dirname(__FILE__), 'lib', 'upload_column') 6 | require File.join(File.dirname(__FILE__), 'lib', 'upload_column', 'rails', 'upload_column_helper') 7 | require File.join(File.dirname(__FILE__), 'lib', 'upload_column', 'rails', 'action_controller_extension') 8 | require File.join(File.dirname(__FILE__), 'lib', 'upload_column', 'rails', 'asset_tag_extension') 9 | 10 | Mime::Type.register "image/png", :png 11 | Mime::Type.register "image/jpeg", :jpg 12 | Mime::Type.register "image/gif", :gif 13 | 14 | UploadColumn::Root = RAILS_ROOT -------------------------------------------------------------------------------- /lib/upload_column.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'upload_column', 'sanitized_file.rb') 2 | require File.join(File.dirname(__FILE__), 'upload_column', 'uploaded_file.rb') 3 | require File.join(File.dirname(__FILE__), 'upload_column', 'magic_columns.rb') 4 | require File.join(File.dirname(__FILE__), 'upload_column', 'active_record_extension.rb') 5 | require File.join(File.dirname(__FILE__), 'upload_column', 'manipulators', 'rmagick.rb') 6 | require File.join(File.dirname(__FILE__), 'upload_column', 'manipulators', 'image_science.rb') 7 | require File.join(File.dirname(__FILE__), 'upload_column', 'configuration.rb') 8 | 9 | 10 | ActiveRecord::Base.send(:include, UploadColumn::ActiveRecordExtension) -------------------------------------------------------------------------------- /lib/upload_column/active_record_extension.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'tempfile' 3 | 4 | module UploadColumn 5 | 6 | Column = Struct.new(:name, :options) 7 | 8 | module ActiveRecordExtension 9 | 10 | def self.append_features(base) #:nodoc: 11 | super 12 | base.extend(ClassMethods) 13 | base.after_save :save_uploaded_files 14 | end 15 | 16 | private 17 | 18 | def save_uploaded_files 19 | @files.each { |k, v| v.send(:save) if v and v.tempfile? } if @files 20 | end 21 | 22 | def get_upload_column(name) 23 | options = options_for_column(name) #TODO: Spec this! 24 | @files ||= {} 25 | return nil if @files[name].is_a?(UploadColumn::IntegrityError) 26 | @files[name] ||= if self[name] then UploadColumn::UploadedFile.retrieve(self[name], self, name, options) else nil end 27 | end 28 | 29 | def set_upload_column(name, file) 30 | options = options_for_column(name) 31 | @files ||= {} 32 | if file.nil? 33 | @files[name], self[name] = nil 34 | else 35 | begin 36 | if uploaded_file = UploadColumn::UploadedFile.upload(file, self, name, options) 37 | self[name] = uploaded_file.actual_filename 38 | @files[name] = uploaded_file 39 | end 40 | rescue IntegrityError => e 41 | @files[name] = e 42 | end 43 | end 44 | end 45 | 46 | def get_upload_column_temp(name) 47 | @files[name].temp_value if @files and @files[name].respond_to?(:temp_value) 48 | end 49 | 50 | def set_upload_column_temp(name, path) 51 | options = options_for_column(name) 52 | @files ||= {} 53 | return if path.nil? or path.empty? 54 | unless @files[name] and @files[name].new_file? 55 | @files[name] = UploadColumn::UploadedFile.retrieve_temp(path, self, name, options) 56 | self[name] = @files[name].actual_filename 57 | end 58 | end 59 | 60 | def options_for_column(name) 61 | return self.class.reflect_on_upload_columns[name].options.reverse_merge(UploadColumn.configuration) 62 | end 63 | 64 | # weave in the magic column methods 65 | include UploadColumn::MagicColumns 66 | 67 | module ClassMethods 68 | 69 | # handle the +attr+ attribute as an "upload-column" field, generating additional methods as explained 70 | # in the README. You should pass the attribute's name as a symbol, like this: 71 | # 72 | # upload_column :picture 73 | # 74 | # +upload_column+ can manipulate file with the following options: 75 | # [+versions+] Creates different versions of the file, can be an Array or a Hash, in the latter case the values of the Hash will be passed to the manipulator 76 | # [+manipulator+] Takes a module that must have a method called process! that takes a single argument. Use this in conjucntion with :versions and :process 77 | # [+process+] This instrucion is passed to the manipulators process! method. 78 | # 79 | # you can customize file storage with the following: 80 | # [+store_dir+] Determines where the file will be stored permanently, you can pass a String or a Proc that takes the current instance and the attribute name as parameters, see the +README+ for detaills. 81 | # [+tmp_dir+] Determines where the file will be stored temporarily before it is stored to its final location, you can pass a String or a Proc that takes the current instance and the attribute name as parameters, see the +README+ for detaills. 82 | # [+old_files+] Determines what happens when a file becomes outdated. It can be set to one of :keep, :delete and :replace. If set to :keep UploadColumn will always keep old files, and if set to :delete it will always delete them. If it's set to :replace, the file will be replaced when a new one is uploaded, but will be kept when the associated object is deleted. Default to :delete. 83 | # [+permissions+] Specify the Unix permissions to be used with UploadColumn. Defaults to 0644. Remember that permissions are usually counted in octal and that in Ruby octal numbers start with a zero, so 0644 != 644. 84 | # [+root_dir+] The root path where image will be stored, it will be prepended to store_dir and tmp_dir 85 | # 86 | # it also accepts the following, less common options: 87 | # [+web_root+] Prepended to all addresses returned by UploadColumn::UploadedFile.url 88 | # [+extensions+] A white list of files that can be used together with validates_integrity_of to secure your uploads against malicious files. 89 | # [+fix_file_extensions+] Try to fix the file's extension based on its mime-type, note that this does not give you any security, to make sure that no dangerous files are uploaded, use +validates_integrity_of+. This defaults to true. 90 | # [+get_content_type_from_file_exec+] If this is set to true, UploadColumn::SanitizedFile will use a *nix exec to try to figure out the content type of the uploaded file. 91 | def upload_column(name, options = {}) 92 | @upload_columns ||= {} 93 | @upload_columns[name] = Column.new(name, options) 94 | 95 | define_method( name ) { get_upload_column(name) } 96 | define_method( "#{name}=" ) { |file| set_upload_column(name, file) } 97 | 98 | define_submethod( name, "temp" ) { get_upload_column_temp(name) } 99 | define_submethod( name, "temp=" ) { |path| set_upload_column_temp(name, path) } 100 | 101 | define_submethod( name, "public_path" ) { get_upload_column(name).public_path rescue nil } 102 | define_submethod( name, "path" ) { get_upload_column(name).path rescue nil } 103 | 104 | if options[:versions] 105 | options[:versions].each do |k, v| 106 | define_submethod( name, k ) { get_upload_column(name).send(k) rescue nil } 107 | define_submethod( name, k, "public_path" ) { get_upload_column(name).send(k).public_path rescue nil } 108 | define_submethod( name, k, "path" ) { get_upload_column(name).send(k).path rescue nil } 109 | end 110 | end 111 | end 112 | 113 | def image_column(name, options={}) 114 | upload_column(name, options.reverse_merge(UploadColumn.image_column_configuration)) 115 | end 116 | 117 | # Validates whether the images extension is in the array passed to :extensions. 118 | # By default this is the UploadColumn.extensions array 119 | # 120 | # Use this to prevent upload of files which could potentially damage your system, 121 | # such as executables or script files (.rb, .php, etc...). 122 | def validates_integrity_of(*attr_names) 123 | configuration = { :message => "is not of a valid file type." } 124 | configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) 125 | 126 | attr_names.each { |name| self.reflect_on_upload_columns[name].options[:validate_integrity] = true } 127 | 128 | validates_each(attr_names, configuration) do |record, attr, value| 129 | value = record.instance_variable_get('@files')[attr] 130 | record.errors.add(attr, value.message) if value.is_a?(IntegrityError) 131 | end 132 | end 133 | 134 | # returns a hash of all UploadColumns defined on the model and their options. 135 | def reflect_on_upload_columns 136 | @upload_columns || {} 137 | end 138 | 139 | private 140 | 141 | def define_submethod(name, *subs, &b) 142 | define_method([name, subs].join('_'), &b) 143 | end 144 | 145 | # This is mostly for testing 146 | def reset_upload_columns 147 | @upload_columns = {} 148 | end 149 | 150 | end 151 | 152 | end 153 | 154 | end -------------------------------------------------------------------------------- /lib/upload_column/configuration.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn 2 | 3 | mattr_accessor :configuration, :image_column_configuration, :extensions, :image_extensions 4 | 5 | self.extensions = %w(asf ai avi doc dvi dwg eps gif gz jpg jpeg mov mp3 mpeg odf pac pdf png ppt psd swf swx tar tar.gz torrent txt wmv wav xls zip) 6 | self.image_extensions = %w(jpg jpeg gif png) 7 | 8 | DEFAULT_CONFIGURATION = { 9 | :tmp_dir => 'tmp', 10 | :store_dir => proc{ |r, f| f.attribute.to_s }, 11 | :root_dir => File.join(RAILS_ROOT, 'public'), 12 | :get_content_type_from_file_exec => true, 13 | :fix_file_extensions => false, 14 | :process => nil, 15 | :permissions => 0644, 16 | :extensions => self.extensions, 17 | :web_root => '', 18 | :manipulator => nil, 19 | :versions => nil, 20 | :validate_integrity => false 21 | } 22 | 23 | self.configuration = UploadColumn::DEFAULT_CONFIGURATION.clone 24 | self.image_column_configuration = { 25 | :manipulator => UploadColumn::Manipulators::RMagick, 26 | :root_dir => File.join(RAILS_ROOT, 'public', 'images'), 27 | :web_root => '/images', 28 | :extensions => self.image_extensions 29 | }.freeze 30 | 31 | def self.configure 32 | yield ConfigurationProxy.new 33 | end 34 | 35 | def self.reset_configuration 36 | self.configuration = UploadColumn::DEFAULT_CONFIGURATION.clone 37 | end 38 | 39 | class ConfigurationProxy 40 | def method_missing(method, value) 41 | if name = (method.to_s.match(/^(.*?)=$/) || [])[1] 42 | UploadColumn.configuration[name.to_sym] = value 43 | else 44 | super 45 | end 46 | end 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /lib/upload_column/magic_columns.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn 2 | module MagicColumns 3 | 4 | def self.included(base) 5 | super 6 | base.send :alias_method_chain, :set_upload_column, :magic_columns 7 | base.send :alias_method_chain, :set_upload_column_temp, :magic_columns 8 | base.send :alias_method_chain, :save_uploaded_files, :magic_columns 9 | end 10 | 11 | def set_upload_column_with_magic_columns(name, file) 12 | set_upload_column_without_magic_columns(name, file) 13 | evaluate_magic_columns_for_upload_column(name) 14 | end 15 | 16 | def set_upload_column_temp_with_magic_columns(name, path) 17 | set_upload_column_temp_without_magic_columns(name, path) 18 | evaluate_magic_columns_for_upload_column(name) 19 | end 20 | 21 | def save_uploaded_files_with_magic_columns 22 | save_uploaded_files_without_magic_columns 23 | self.class.reflect_on_upload_columns.each do |name, column| 24 | evaluate_magic_columns_for_upload_column(name) 25 | end 26 | end 27 | 28 | private 29 | 30 | def evaluate_magic_columns_for_upload_column(name) 31 | 32 | self.class.column_names.each do |column_name| 33 | 34 | statement, predicate = column_name.split('_', 2) 35 | 36 | if statement and predicate and name.to_s == statement and not self.read_attribute(column_name.to_sym) 37 | uploaded_file = self.send(:get_upload_column, name.to_sym) 38 | 39 | self.write_attribute(column_name.to_sym, handle_predicate(uploaded_file, predicate)) 40 | end 41 | 42 | end 43 | end 44 | 45 | def handle_predicate(uploaded_file, predicate) 46 | return uploaded_file.send(predicate.to_sym) if uploaded_file.respond_to?(predicate.to_sym) 47 | end 48 | 49 | end 50 | end -------------------------------------------------------------------------------- /lib/upload_column/manipulators/image_science.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn 2 | module Manipulators 3 | 4 | module ImageScience 5 | 6 | attr_reader :width, :height 7 | 8 | def load_manipulator_dependencies #:nodoc: 9 | require 'image_science' 10 | end 11 | 12 | def process!(instruction) 13 | if instruction.to_s =~ /^c(\d+x\d+)/ 14 | crop_resized!($1) 15 | elsif instruction.to_s =~ /\d+x\d+/ 16 | resize!(instruction) 17 | end 18 | end 19 | 20 | # Resize the image so that it will not exceed the dimensions passed 21 | # via geometry, geometry should be a string, formatted like '200x100' where 22 | # the first number is the height and the second is the width 23 | def resize!( geometry ) 24 | ::ImageScience.with_image(self.path) do |img| 25 | width, height = extract_dimensions(img.width, img.height, geometry) 26 | img.resize( width, height ) do |file| 27 | file.save( self.path ) 28 | end 29 | end 30 | end 31 | 32 | # Resize and crop the image so that it will have the exact dimensions passed 33 | # via geometry, geometry should be a string, formatted like '200x100' where 34 | # the first number is the height and the second is the width 35 | def crop_resized!( geometry ) 36 | ::ImageScience.with_image(self.path) do |img| 37 | new_width, new_height = geometry.split('x').map{|i| i.to_i } 38 | 39 | width, height = extract_dimensions_for_crop(img.width, img.height, geometry) 40 | x_offset, y_offset = extract_placement_for_crop(width, height, geometry) 41 | 42 | img.resize( width, height ) do |i2| 43 | 44 | i2.with_crop( x_offset, y_offset, new_width + x_offset, new_height + y_offset) do |file| 45 | file.save( self.path ) 46 | end 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def extract_dimensions(width, height, new_geometry, type = :resize) 54 | new_width, new_height = convert_geometry(new_geometry) 55 | 56 | aspect_ratio = width.to_f / height.to_f 57 | new_aspect_ratio = new_width / new_height 58 | 59 | if (new_aspect_ratio > aspect_ratio) ^ ( type == :crop ) # Image is too wide, the caret is the XOR operator 60 | new_width, new_height = [ (new_height * aspect_ratio), new_height] 61 | else #Image is too narrow 62 | new_width, new_height = [ new_width, (new_width / aspect_ratio)] 63 | end 64 | 65 | [new_width, new_height].collect! { |v| v.round } 66 | end 67 | 68 | def extract_dimensions_for_crop(width, height, new_geometry) 69 | extract_dimensions(width, height, new_geometry, :crop) 70 | end 71 | 72 | def extract_placement_for_crop(width, height, new_geometry) 73 | new_width, new_height = convert_geometry(new_geometry) 74 | x_offset = (width / 2.0) - (new_width / 2.0) 75 | y_offset = (height / 2.0) - (new_height / 2.0) 76 | [x_offset, y_offset].collect! { |v| v.round } 77 | end 78 | 79 | def convert_geometry(geometry) 80 | geometry.split('x').map{|i| i.to_f } 81 | end 82 | 83 | end 84 | 85 | end 86 | end -------------------------------------------------------------------------------- /lib/upload_column/manipulators/rmagick.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn 2 | 3 | UploadError = Class.new(StandardError) unless defined?(UploadError) 4 | ManipulationError = Class.new(UploadError) unless defined?(ManipulationError) 5 | 6 | module Manipulators 7 | 8 | module RMagick 9 | 10 | def load_manipulator_dependencies #:nodoc: 11 | require 'RMagick' 12 | end 13 | 14 | def process!(instruction = nil, &block) 15 | if instruction.is_a?(Proc) 16 | manipulate!(&instruction) 17 | elsif instruction.to_s =~ /^c(\d+x\d+)$/ 18 | crop_resized!($1) 19 | elsif instruction.to_s =~ /^(\d+x\d+)$/ 20 | resize!($1) 21 | end 22 | manipulate!(&block) if block 23 | end 24 | 25 | # Convert the image to format 26 | def convert!(format) 27 | manipulate! do |img| 28 | img.format = format.to_s.upcase 29 | img 30 | end 31 | end 32 | 33 | # Resize the image so that it will not exceed the dimensions passed 34 | # via geometry, geometry should be a string, formatted like '200x100' where 35 | # the first number is the height and the second is the width 36 | def resize!( geometry ) 37 | manipulate! do |img| 38 | img.change_geometry( geometry ) do |c, r, i| 39 | i.resize(c,r) 40 | end 41 | end 42 | end 43 | 44 | # Resize and crop the image so that it will have the exact dimensions passed 45 | # via geometry, geometry should be a string, formatted like '200x100' where 46 | # the first number is the height and the second is the width 47 | def crop_resized!( geometry ) 48 | manipulate! do |img| 49 | h, w = geometry.split('x') 50 | img.crop_resized(h.to_i,w.to_i) 51 | end 52 | end 53 | 54 | def manipulate! 55 | image = ::Magick::Image.read(self.path) 56 | 57 | if image.size > 1 58 | list = ::Magick::ImageList.new 59 | image.each do |frame| 60 | list << yield( frame ) 61 | end 62 | list.write(self.path) 63 | else 64 | yield( image.first ).write(self.path) 65 | end 66 | rescue ::Magick::ImageMagickError => e 67 | # this is a more meaningful error message, which we could catch later 68 | raise ManipulationError.new("Failed to manipulate with rmagick, maybe it is not an image? Original Error: #{e}") 69 | end 70 | 71 | end 72 | 73 | end 74 | 75 | end -------------------------------------------------------------------------------- /lib/upload_column/rails/action_controller_extension.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn::ActionControllerExtension 2 | 3 | def self.included(base) 4 | base.send :alias_method_chain, :url_for, :uploaded_file_check 5 | base.helper_method :url_for_path 6 | end 7 | 8 | protected 9 | 10 | def url_for_with_uploaded_file_check(options = {}, *parameters_for_method_reference) 11 | if(options.respond_to?(:public_path)) 12 | options.public_path 13 | else 14 | url_for_without_uploaded_file_check(options || {}, *parameters_for_method_reference) 15 | end 16 | end 17 | 18 | def url_for_path(path) 19 | request.protocol + request.host_with_port + path 20 | end 21 | 22 | # You can use +render_image+ in your controllers to render an image 23 | # def picture 24 | # @user = User.find(params[:id]) 25 | # render_image @user.picture 26 | # end 27 | # This of course, is not very useful at all (you could simply have linked to the image itself), 28 | # However it is even possible to pass a block to render_image that allows manipulation using 29 | # RMagick, here the fun begins: 30 | # def solarize_picture 31 | # @user = User.find(params[:id]) 32 | # render_image @user.picture do |img| 33 | # img = img.segment 34 | # img.solarize 35 | # end 36 | # end 37 | # Note that like in UploadColumn::BaseUploadedFile.process you will need to 'carry' the image 38 | # since most Rmagick methods do not modify the image itself but rather return the result of the 39 | # transformation. 40 | # 41 | # Instead of passing an upload_column object to +render_image+ you can even pass a path String, 42 | # if you do you will have to pass a :mime-type option as well though. 43 | def render_image( file, options = {} ) 44 | format = if options.is_a?(Hash) then options[:force_format] else nil end 45 | mime_type = if options.is_a?(String) then options else options[:mime_type] end 46 | mime_type ||= file.mime_type 47 | path = if file.is_a?( String ) then file else file.path end 48 | headers["Content-Type"] = mime_type unless format 49 | 50 | if block_given? or format 51 | img = ::Magick::Image::read(path).first 52 | img = yield( img ) if block_given? 53 | img.format = format.to_s.upcase if format 54 | render :text => img.to_blob, :layout => false 55 | else 56 | send_file( path ) 57 | end 58 | end 59 | end 60 | 61 | ActionController::Base.send(:include, UploadColumn::ActionControllerExtension) -------------------------------------------------------------------------------- /lib/upload_column/rails/asset_tag_extension.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn::AssetTagExtension 2 | 3 | def self.included(base) 4 | base.send :alias_method_chain, :image_tag, :uploaded_file_check 5 | end 6 | 7 | def image_tag_with_uploaded_file_check(source, options = {}) 8 | if(source.respond_to?(:public_path)) 9 | image_tag_without_uploaded_file_check(source.public_path, options) 10 | else 11 | image_tag_without_uploaded_file_check(source, options) 12 | end 13 | end 14 | 15 | end 16 | 17 | ActionView::Helpers::AssetTagHelper.send(:include, UploadColumn::AssetTagExtension) -------------------------------------------------------------------------------- /lib/upload_column/rails/upload_column_helper.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn::UploadColumnHelper 2 | 3 | # Returns an input tag of the "file" type tailored for accessing an upload_column field 4 | # (identified by method) on an object assigned to the template (identified by object). 5 | # Additional options on the input tag can be passed as a hash with options. 6 | # 7 | # Example (call, result) 8 | # upload_column_field( :user, :picture ) 9 | # 10 | # 11 | # 12 | # Note: if you use file_field instead of upload_column_field, the file will not be 13 | # stored across form redisplays. 14 | def upload_column_field(object, method, options={}) 15 | file_field(object, method, options) + hidden_field(object, method.to_s + '_temp') 16 | end 17 | 18 | # A helper method for creating a form tag to use with uploadng files, 19 | # it works exactly like Rails' form_tag, except that :multipart is always true 20 | def upload_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc) 21 | options[:multipart] = true 22 | form_tag( url_for_options, options, *parameters_for_url, &proc ) 23 | end 24 | 25 | # A helper method for creating a form tag to use with uploadng files, 26 | # it works exactly like Rails' form_for, except that :multipart is always true 27 | def upload_form_for(*args, &block) 28 | options = args.extract_options! 29 | options[:html] ||= {} 30 | options[:html][:multipart] = true 31 | args.push(options) 32 | 33 | form_for(*args, &block) 34 | end 35 | 36 | end 37 | 38 | class ActionView::Helpers::FormBuilder #:nodoc: 39 | self.field_helpers += ['upload_column_field'] 40 | def upload_column_field(method, options = {}) 41 | @template.send(:upload_column_field, @object_name, method, options.merge(:object => @object)) 42 | end 43 | end 44 | 45 | ActionView::Base.send(:include, UploadColumn::UploadColumnHelper) -------------------------------------------------------------------------------- /lib/upload_column/sanitized_file.rb: -------------------------------------------------------------------------------- 1 | begin; require 'mime/types'; rescue Exception; end 2 | 3 | require 'fileutils' 4 | 5 | module UploadColumn 6 | # Sanitize is a base class that takes care of all the dirtywork when dealing with file uploads. 7 | # it is subclassed as UploadedFile in UploadColumn, which does most of the upload magic, but if 8 | # you want to roll you own uploading system, SanitizedFile might be for you since it takes care 9 | # of a lot of the unfun stuff. 10 | # 11 | # Usage is pretty simple, just do SanitizedFile.new(some_uploaded_file) and you're good to go 12 | # you can now use #copy_to and #move_to to place the file wherever you want, whether it is a StringIO 13 | # or a TempFile. 14 | # 15 | # SanitizedFile also deals with content type detection, which it does either through the 'file' *nix exec 16 | # or (if you are stuck on Windows) through the MIME::Types library (not to be confused with Rails' Mime class!). 17 | class SanitizedFile 18 | 19 | attr_reader :basename, :extension 20 | 21 | def initialize(file, options = {}) 22 | @options = options 23 | if file && file.instance_of?(String) && !file.empty? 24 | @path = file 25 | self.filename = File.basename(file) 26 | else 27 | @file = file 28 | self.filename = self.original_filename unless self.empty? 29 | end 30 | end 31 | 32 | # Returns the filename before sanitation took place 33 | def original_filename 34 | @original_filename ||= if @file and @file.respond_to?(:original_filename) 35 | @file.original_filename 36 | elsif self.path 37 | File.basename(self.path) 38 | end 39 | end 40 | 41 | # Returns the files properly sanitized filename. 42 | def filename 43 | @filename ||= (self.extension && !self.extension.empty?) ? "#{self.basename}.#{self.extension}" : self.basename 44 | end 45 | 46 | # Returns the file's size 47 | def size 48 | return @file.size if @file.respond_to?(:size) 49 | File.size(self.path) rescue nil 50 | end 51 | 52 | # Returns the full path to the file 53 | def path 54 | @path ||= File.expand_path(@file.path) rescue nil 55 | end 56 | 57 | # Checks if the file is empty. 58 | def empty? 59 | (@file.nil? && @path.nil?) || self.size.nil? || self.size.zero? 60 | end 61 | 62 | # Checks if the file exists 63 | def exists? 64 | File.exists?(self.path) if self.path 65 | end 66 | 67 | # Moves the file to 'path' 68 | def move_to(path) 69 | if copy_file(path) 70 | # FIXME: This gets pretty broken in UploadedFile. E.g. moving avatar-thumb.jpg will change the filename 71 | # to avatar-thumb-thumb.jpg 72 | @basename, @extension = split_extension(File.basename(path)) 73 | @file = nil 74 | @filename = nil 75 | @path = path 76 | end 77 | end 78 | 79 | # Copies the file to 'path' and returns a new SanitizedFile that points to the copy. 80 | def copy_to(path) 81 | copy = self.clone 82 | copy.move_to(path) 83 | return copy 84 | end 85 | 86 | # Returns the content_type of the file as determined through the MIME::Types library or through a *nix exec. 87 | def content_type 88 | unless content_type = get_content_type_from_exec || get_content_type_from_mime_types 89 | content_type ||= @file.content_type.chomp if @file.respond_to?(:content_type) and @file.content_type 90 | end 91 | return content_type 92 | end 93 | 94 | private 95 | 96 | def copy_file(path) 97 | unless self.empty? 98 | # create the directory if it doesn't exist 99 | FileUtils.mkdir_p(File.dirname(path)) unless File.exists?(File.dirname(path)) 100 | # stringios don't have a path and can't be copied 101 | if not self.path and @file.respond_to?(:read) 102 | @file.rewind # Make sure we are at the beginning of the buffer 103 | File.open(path, "wb") { |f| f.write(@file.read) } 104 | else 105 | begin 106 | FileUtils.cp(self.path, path) 107 | rescue ArgumentError 108 | end 109 | end 110 | File.chmod(@options[:permissions], path) if @options[:permissions] 111 | return true 112 | end 113 | end 114 | 115 | def filename=(filename) 116 | basename, extension = split_extension(filename) 117 | @basename = sanitize(basename) 118 | @extension = correct_file_extension(extension) 119 | end 120 | 121 | # tries to identify the mime-type of file and correct self's extension 122 | # based on the found mime-type 123 | def correct_file_extension(ext) 124 | if @options[:fix_file_extensions] && defined?(MIME::Types) 125 | if mimes = MIME::Types[self.content_type] 126 | return mimes.first.extensions.first unless mimes.first.extensions.empty? 127 | end 128 | end 129 | return ext.downcase 130 | end 131 | 132 | # Try to use *nix exec to fetch content type 133 | def get_content_type_from_exec 134 | if @options[:get_content_type_from_file_exec] and not self.path.empty? 135 | return system_call(%(file -bi "#{self.path}")).chomp.scan(/^[a-z0-9\-_]+\/[a-z0-9\-_]+/).first 136 | end 137 | rescue 138 | nil 139 | end 140 | 141 | def system_call(command) 142 | `#{command}` 143 | end 144 | 145 | def get_content_type_from_mime_types 146 | if @extension and defined?(MIME::Types) 147 | mimes = MIME::Types.of(@extension) 148 | return mimes.first.content_type rescue nil 149 | end 150 | end 151 | 152 | def sanitize(name) 153 | # Sanitize the filename, to prevent hacking 154 | name = File.basename(name.gsub("\\", "/")) # work-around for IE 155 | name.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_") 156 | name = "_#{name}" if name =~ /^\.+$/ 157 | name = "unnamed" if name.size == 0 158 | return name.downcase 159 | end 160 | 161 | def split_extension(fn) 162 | # regular expressions to try for identifying extensions 163 | ext_regexps = [ 164 | /^(.+)\.([^\.]{1,3}\.[^\.]{1,4})$/, # matches "something.tar.gz" 165 | /^(.+)\.([^\.]+)$/ # matches "something.jpg" 166 | ] 167 | ext_regexps.each do |regexp| 168 | if fn =~ regexp 169 | return $1, $2 170 | end 171 | end 172 | return fn, "" # In case we weren't able to split the extension 173 | end 174 | 175 | end 176 | end -------------------------------------------------------------------------------- /lib/upload_column/uploaded_file.rb: -------------------------------------------------------------------------------- 1 | module UploadColumn 2 | 3 | class UploadError < StandardError #:nodoc: 4 | end 5 | class IntegrityError < UploadError #:nodoc: 6 | end 7 | class TemporaryPathMalformedError < UploadError #:nodoc: 8 | end 9 | class UploadNotMultipartError < UploadError #:nodoc: 10 | end 11 | 12 | TempValueRegexp = %r{^((?:\d+\.)+\d+)/([^/;]+)(?:;([^/;]+))?$} 13 | 14 | 15 | # When you call an upload_column field, an instance of this class will be returned. 16 | # 17 | # Suppose a +User+ model has a +picture+ upload_column, like so: 18 | # class User < ActiveRecord::Base 19 | # upload_column :picture 20 | # end 21 | # Now in our controller we did: 22 | # @user = User.find(params[:id]) 23 | # We could then access the file: 24 | # @user.picture.url 25 | # Which would output the url to the file (assuming it is stored in /public/) 26 | # = Versions 27 | # If we had instead added different versions in our model 28 | # upload_column :picture, :versions => [:thumb, :large] 29 | # Then we could access them like so: 30 | # @user.picture.thumb.url 31 | # See the +README+ for more detaills. 32 | class UploadedFile < SanitizedFile 33 | 34 | attr_reader :instance, :attribute, :options, :versions 35 | attr_accessor :suffix 36 | 37 | class << self 38 | 39 | # upload a file. In most cases you want to pass the ActiveRecord instance and the attribute 40 | # name as well as the file. For a more bare-bones approach, check out SanitizedFile. 41 | def upload(file, instance = nil, attribute = nil, options = {}) #:nodoc: 42 | uf = self.new(:upload, file, instance, attribute, options) 43 | return uf.empty? ? nil : uf 44 | end 45 | 46 | # Retrieve a file from the filesystem, based on the calculated store_dir and the filename 47 | # stored in the database. 48 | def retrieve(filename, instance = nil, attribute = nil, options = {}) #:nodoc: 49 | self.new(:retrieve, filename, instance, attribute, options) 50 | end 51 | 52 | # Retreieve a file that was stored as a temp file 53 | def retrieve_temp(path, instance = nil, attribute = nil, options = {}) #:nodoc: 54 | self.new(:retrieve_temp, path, instance, attribute, options) 55 | end 56 | 57 | end 58 | 59 | def initialize(mode, file, instance, attribute, options={}) 60 | # TODO: the options are always reverse merged in here, in case UploadedFile has 61 | # been initialized outside UploadColumn proper, this is not a very elegant solution, imho. 62 | @options = options.reverse_merge(UploadColumn.configuration) 63 | @instance = instance 64 | @attribute = attribute 65 | @suffix = options[:suffix] 66 | 67 | load_manipulator 68 | 69 | case mode 70 | when :upload 71 | if file and file.is_a?(String) and not file.empty? 72 | raise UploadNotMultipartError.new("Do not know how to handle a string with value '#{file}' that was uploaded. Check if the form's encoding has been set to 'multipart/form-data'.") 73 | end 74 | 75 | super(file, @options) 76 | 77 | unless empty? 78 | if options[:validate_integrity] 79 | raise UploadError.new("No list of valid extensions supplied.") unless options[:extensions] 80 | raise IntegrityError.new("has an extension that is not allowed.") unless options[:extensions].include?(extension) 81 | end 82 | 83 | @temp_name = generate_tmpname 84 | @new_file = true 85 | 86 | move_to_directory(File.join(tmp_dir, @temp_name)) 87 | 88 | # The original is processed before versions are initialized. 89 | self.process!(@options[:process]) if @options[:process] and self.respond_to?(:process!) 90 | 91 | initialize_versions do |version| 92 | copy_to_version(version) 93 | end 94 | 95 | apply_manipulations_to_versions 96 | 97 | # trigger the _after_upload callback 98 | self.instance.send("#{self.attribute}_after_upload", self) if self.instance.respond_to?("#{self.attribute}_after_upload") 99 | end 100 | when :retrieve 101 | @path = File.join(store_dir, file) 102 | @basename, @extension = split_extension(file) 103 | initialize_versions 104 | when :retrieve_temp 105 | if file and not file.empty? 106 | @temp_name, name, original_filename = file.scan( ::UploadColumn::TempValueRegexp ).first 107 | 108 | if @temp_name and name 109 | @path = File.join(tmp_dir, @temp_name, name) 110 | @basename, @extension = split_extension(name) 111 | @original_filename = original_filename 112 | initialize_versions 113 | else 114 | raise TemporaryPathMalformedError.new("#{file} is not a valid temporary path!") 115 | end 116 | end 117 | else 118 | super(file, @options) 119 | initialize_versions 120 | end 121 | end 122 | 123 | # Returns the directory where tmp files are stored for this UploadedFile, relative to :root_dir 124 | def relative_tmp_dir 125 | parse_dir_options(:tmp_dir) 126 | end 127 | 128 | # Returns the directory where tmp files are stored for this UploadedFile 129 | def tmp_dir 130 | File.expand_path(self.relative_tmp_dir, @options[:root_dir]) 131 | end 132 | 133 | # Returns the directory where files are stored for this UploadedFile, relative to :root_dir 134 | def relative_store_dir 135 | parse_dir_options(:store_dir) 136 | end 137 | 138 | # Returns the directory where files are stored for this UploadedFile 139 | def store_dir 140 | File.expand_path(self.relative_store_dir, @options[:root_dir]) 141 | end 142 | 143 | # Returns the path of the file relative to :root_dir 144 | def relative_path 145 | self.path.sub(File.expand_path(options[:root_dir]) + '/', '') 146 | end 147 | 148 | # returns the full path of the file. 149 | def path; super; end 150 | 151 | # returns the directory where the file is currently stored. 152 | def dir 153 | File.dirname(self.path) 154 | end 155 | 156 | # return true if the file has just been uploaded. 157 | def new_file? 158 | @new_file 159 | end 160 | 161 | # returns the url of the file, by merging the relative path with the web_root option. 162 | def public_path 163 | # TODO: this might present an attack vector if the file is outside the web_root 164 | options[:web_root].to_s + '/' + self.relative_path.gsub("\\", "/") 165 | end 166 | 167 | alias_method :to_s, :public_path 168 | alias_method :url, :public_path 169 | 170 | # this is the value returned when avatar_temp is called, where avatar is an upload_column 171 | def temp_value #:nodoc: 172 | if tempfile? 173 | if original_filename 174 | %(#{@temp_name}/#{filename};#{original_filename}) 175 | else 176 | %(#{@temp_name}/#{filename}) 177 | end 178 | end 179 | end 180 | 181 | def inspect #:nodoc: 182 | "" 183 | end 184 | 185 | def tempfile? 186 | @temp_name 187 | end 188 | 189 | alias_method :actual_filename, :filename 190 | 191 | def filename 192 | unless bn = parse_dir_options(:filename) 193 | bn = [self.basename, self.suffix].compact.join('-') 194 | bn += ".#{self.extension}" unless self.extension.blank? 195 | end 196 | return bn 197 | end 198 | 199 | # TODO: this is a public method, should be specced 200 | def move_to_directory(dir) 201 | p = File.join(dir, self.filename) 202 | if copy_file(p) 203 | @path = p 204 | end 205 | end 206 | 207 | private 208 | 209 | def copy_to_version(version) 210 | copy = self.clone 211 | copy.suffix = version 212 | 213 | if copy_file(File.join(self.dir, copy.filename)) 214 | return copy 215 | end 216 | end 217 | 218 | def initialize_versions 219 | if self.options[:versions] 220 | @versions = {} 221 | 222 | version_keys = options[:versions].is_a?(Hash) ? options[:versions].keys : options[:versions] 223 | 224 | version_keys.each do |version| 225 | 226 | version = version.to_sym 227 | 228 | # Raise an error if the version name is a method on this class 229 | raise ArgumentError.new("#{version} is an illegal name for an UploadColumn version.") if self.respond_to?(version) 230 | 231 | if block_given? 232 | @versions[version] = yield(version) 233 | else 234 | # Copy the file and store it in the versions array 235 | # TODO: this might result in the manipulator not being loaded. 236 | @versions[version] = self.clone #class.new(:open, File.join(self.dir, "#{self.basename}-#{version}.#{self.extension}"), instance, attribute, options.merge(:versions => nil, :suffix => version)) 237 | @versions[version].suffix = version 238 | end 239 | 240 | @versions[version].instance_eval { @path = File.join(self.dir, self.filename) } # ensure path is not cached 241 | 242 | # Add the version methods to the instance 243 | self.instance_eval <<-SRC 244 | def #{version} 245 | self.versions[:#{version}] 246 | end 247 | SRC 248 | end 249 | end 250 | end 251 | 252 | def load_manipulator 253 | if options[:manipulator] 254 | self.extend(options[:manipulator]) 255 | self.load_manipulator_dependencies if self.respond_to?(:load_manipulator_dependencies) 256 | end 257 | end 258 | 259 | def apply_manipulations_to_versions 260 | @versions.each do |k, v| 261 | v.process! @options[:versions][k] 262 | end if @options[:versions].is_a?(Hash) 263 | end 264 | 265 | def save 266 | self.move_to_directory(self.store_dir) 267 | self.versions.each { |version, file| file.move_to_directory(self.store_dir) } if self.versions 268 | @new_file = false 269 | @temp_name = nil 270 | true 271 | end 272 | 273 | def parse_dir_options(option) 274 | if self.instance.respond_to?("#{self.attribute}_#{option}") 275 | self.instance.send("#{self.attribute}_#{option}", self) 276 | else 277 | option = @options[option] 278 | if option.is_a?(Proc) 279 | case option.arity 280 | when 2 281 | option.call(self.instance, self) 282 | when 1 283 | option.call(self.instance) 284 | else 285 | option.call 286 | end 287 | else 288 | option 289 | end 290 | end 291 | end 292 | 293 | def generate_tmpname 294 | now = Time.now 295 | "#{now.to_i}.#{now.usec}.#{Process.pid}" 296 | end 297 | 298 | end 299 | end -------------------------------------------------------------------------------- /spec/active_record_extension_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | gem 'activerecord' 4 | require 'active_record' 5 | 6 | require File.join(File.dirname(__FILE__), '../lib/upload_column') 7 | 8 | class Entry < ActiveRecord::Base; end # setup a basic AR class for testing 9 | 10 | describe "an ActiveRecord class" do 11 | 12 | include UploadColumnSpecHelper 13 | 14 | it "should respond to upload_column" do 15 | Entry.should respond_to(:upload_column) 16 | end 17 | 18 | it "should reflect on upload_columns" do 19 | Entry.send(:reset_upload_columns) 20 | 21 | Entry.upload_column(:avatar) 22 | 23 | Entry.reflect_on_upload_columns[:avatar].should be_an_instance_of(UploadColumn::Column) 24 | Entry.reflect_on_upload_columns[:monkey].should == nil 25 | 26 | Entry.upload_column(:monkey) 27 | 28 | Entry.reflect_on_upload_columns[:avatar].should be_an_instance_of(UploadColumn::Column) 29 | Entry.reflect_on_upload_columns[:monkey].should be_an_instance_of(UploadColumn::Column) 30 | end 31 | 32 | it "should reset upload columns" do 33 | Entry.upload_column(:avatar) 34 | 35 | Entry.reflect_on_upload_columns[:avatar].should be_an_instance_of(UploadColumn::Column) 36 | 37 | Entry.send(:reset_upload_columns) 38 | 39 | Entry.reflect_on_upload_columns[:avatar].should == nil 40 | end 41 | 42 | end 43 | 44 | describe "an Active Record class with an upload_column" do 45 | 46 | include UploadColumnSpecHelper 47 | 48 | it "should add accessor methods" do 49 | # use a name that hasn't been used before! 50 | entry = disconnected_model(Entry) 51 | entry.should_not respond_to(:llama) 52 | entry.should_not respond_to(:llama_temp) 53 | entry.should_not respond_to(:llama=) 54 | entry.should_not respond_to(:llama_temp=) 55 | 56 | Entry.upload_column(:llama) 57 | 58 | entry = disconnected_model(Entry) 59 | 60 | entry.should respond_to(:llama) 61 | entry.should respond_to(:llama_temp) 62 | entry.should respond_to(:llama=) 63 | entry.should respond_to(:llama_temp=) 64 | end 65 | 66 | it "should save the name of the column to be reflected upon" do 67 | Entry.upload_column(:walruss) 68 | Entry.reflect_on_upload_columns[:walruss].name.should == :walruss 69 | end 70 | 71 | it "should save the options to be reflected upon" do 72 | options = { :donkey => true } 73 | 74 | Entry.upload_column(:walruss, options) 75 | 76 | Entry.reflect_on_upload_columns[:walruss].options.should == options 77 | end 78 | end 79 | 80 | describe "an Active Record with no upload_column" do 81 | 82 | before(:all) do 83 | class Monkey < ActiveRecord::Base; end 84 | end 85 | 86 | it "should have no uploads_column" do 87 | Monkey.reflect_on_upload_columns.should == {} 88 | end 89 | 90 | it "should be instantiable" do 91 | Monkey.stub!(:columns).and_return([]) 92 | Monkey.new 93 | end 94 | 95 | end 96 | 97 | describe "uploading a file" do 98 | 99 | include UploadColumnSpecHelper 100 | 101 | before do 102 | setup_standard_mocking 103 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 104 | end 105 | 106 | it "should pass it to UploadedFile and remember it" do 107 | @entry.avatar.should == nil 108 | @entry.avatar = @file 109 | @entry.avatar.should == @uploaded_file 110 | end 111 | 112 | it "should set the attribute on the ActiveRecord" do 113 | @entry.should_receive(:[]=).with(:avatar, 'monkey.png') 114 | @entry.avatar = @file 115 | end 116 | 117 | end 118 | 119 | describe "uploading an empty String" do 120 | 121 | include UploadColumnSpecHelper 122 | 123 | before do 124 | setup_standard_mocking 125 | end 126 | 127 | it "should do nothing" do 128 | UploadColumn::UploadedFile.should_receive(:upload).with("", @entry, :avatar, @options).and_return(nil) 129 | @entry.avatar.should == nil 130 | @entry.avatar = "" 131 | @entry.avatar.should == nil 132 | end 133 | 134 | it "shouldn't affect an already uploaded file" do 135 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 136 | @entry.avatar = @file 137 | @entry.avatar.should == @uploaded_file 138 | 139 | UploadColumn::UploadedFile.should_receive(:upload).with("", @entry, :avatar, @options).and_return(nil) 140 | @entry.avatar = "" 141 | @entry.avatar.should == @uploaded_file 142 | end 143 | 144 | end 145 | 146 | describe "setting nil explicitly" do 147 | 148 | include UploadColumnSpecHelper 149 | 150 | before do 151 | setup_standard_mocking 152 | end 153 | 154 | it "should reset the column" do 155 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 156 | @entry.avatar = @file 157 | @entry.avatar.should == @uploaded_file 158 | 159 | @entry.avatar = nil 160 | @entry.avatar.should == nil 161 | end 162 | end 163 | 164 | describe "an upload_column with a value stored in the database and no uploaded_file" do 165 | 166 | include UploadColumnSpecHelper 167 | 168 | before do 169 | @options = mock('options', :null_object => true) 170 | Entry.upload_column(:avatar, @options) 171 | 172 | @entry = disconnected_model(Entry) 173 | @entry.stub!(:inspect).and_return('<#Entry>') 174 | @string = mock('some string') 175 | @entry.should_receive(:[]).with(:avatar).at_least(:once).and_return(@string) 176 | end 177 | 178 | it "should retrieve the file from the database" do 179 | uploaded_file = mock('uploaded file') 180 | 181 | UploadColumn::UploadedFile.should_receive(:retrieve).with(@string, @entry, :avatar, @options).and_return(uploaded_file) 182 | 183 | @entry.avatar.should == uploaded_file 184 | end 185 | end 186 | 187 | describe "saving uploaded files" do 188 | 189 | include UploadColumnSpecHelper 190 | 191 | before do 192 | setup_standard_mocking 193 | end 194 | 195 | it "should call save on the uploaded file if they are temporary files" do 196 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 197 | 198 | @uploaded_file.should_receive(:tempfile?).and_return(true) 199 | @uploaded_file.should_receive(:save) 200 | @entry.avatar = @file 201 | 202 | @entry.send(:save_uploaded_files) 203 | end 204 | 205 | it "should not call save on the uploaded file if they are not temporary files" do 206 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 207 | 208 | @uploaded_file.should_receive(:tempfile?).and_return(false) 209 | @uploaded_file.should_not_receive(:save) 210 | @entry.avatar = @file 211 | 212 | @entry.send(:save_uploaded_files) 213 | end 214 | 215 | it "should happen automatically" do 216 | # TODO: hmmm, how to test this? do we have to rely on an integration test? 217 | #@entry.should_receive(:save_uploaded_files) 218 | #@entry.save 219 | end 220 | 221 | end 222 | 223 | describe "fetching a temp value" do 224 | 225 | include UploadColumnSpecHelper 226 | 227 | setup do 228 | setup_standard_mocking 229 | 230 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 231 | 232 | @temp_value = '12345.1234.12345/somewhere.png' 233 | 234 | @uploaded_file.should_receive(:temp_value).and_return(@temp_value) 235 | @entry.avatar = @file 236 | end 237 | 238 | it "should fetch the value from the uploaded file" do 239 | @entry.avatar_temp.should == @temp_value 240 | end 241 | 242 | end 243 | 244 | describe "assigning a tempfile" do 245 | 246 | include UploadColumnSpecHelper 247 | 248 | setup do 249 | setup_standard_mocking 250 | end 251 | 252 | it "should not override a new file" do 253 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 254 | @uploaded_file.stub!(:new_file?).and_return(true) 255 | @entry.avatar = @file 256 | 257 | temp_value = '12345.1234.12345/somewhere.png' 258 | 259 | UploadColumn::UploadedFile.should_not_receive(:retrieve_temp) 260 | @entry.avatar_temp = temp_value 261 | 262 | @entry.avatar.should == @uploaded_file 263 | end 264 | 265 | it "should override a file that is not new" do 266 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 267 | @uploaded_file.stub!(:new_file?).and_return(false) 268 | @entry.avatar = @file 269 | 270 | temp_value = '12345.1234.12345/somewhere.png' 271 | 272 | retrieved_file = mock('a retrieved file') 273 | retrieved_file.should_receive(:actual_filename).and_return('walruss.png') 274 | UploadColumn::UploadedFile.should_receive(:retrieve_temp).with(temp_value, @entry, :avatar, @options).and_return(retrieved_file) 275 | @entry.should_receive(:[]=).with(:avatar, 'walruss.png') 276 | 277 | @entry.avatar_temp = temp_value 278 | 279 | @entry.avatar.should == retrieved_file 280 | end 281 | 282 | it "should set the file if there is none" do 283 | 284 | temp_value = '12345.1234.12345/somewhere.png' 285 | 286 | retrieved_file = mock('a retrieved file') 287 | retrieved_file.should_receive(:actual_filename).and_return('walruss.png') 288 | UploadColumn::UploadedFile.should_receive(:retrieve_temp).with(temp_value, @entry, :avatar, @options).and_return(retrieved_file) 289 | @entry.should_receive(:[]=).with(:avatar, 'walruss.png') 290 | 291 | @entry.avatar_temp = temp_value 292 | 293 | @entry.avatar.should == retrieved_file 294 | end 295 | 296 | end 297 | 298 | describe "assigning nil to temp" do 299 | 300 | include UploadColumnSpecHelper 301 | 302 | before(:each) do 303 | setup_standard_mocking 304 | end 305 | 306 | it "should do nothing" do 307 | UploadColumn::UploadedFile.stub!(:upload).and_return(@uploaded_file) 308 | @uploaded_file.stub!(:new_file?).and_return(false) 309 | @entry.avatar = @file 310 | 311 | UploadColumn::UploadedFile.should_not_receive(:retrieve_temp) 312 | @entry.should_not_receive(:[]=) 313 | 314 | lambda { 315 | @entry.avatar_temp = nil 316 | }.should_not change(@entry, :avatar) 317 | end 318 | end 319 | 320 | describe "assigning a blank string to temp" do 321 | 322 | include UploadColumnSpecHelper 323 | 324 | before(:each) do 325 | setup_standard_mocking 326 | end 327 | 328 | it "should do nothing" do 329 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 330 | @uploaded_file.stub!(:new_file?).and_return(false) 331 | @entry.avatar = @file 332 | 333 | UploadColumn::UploadedFile.should_not_receive(:retrieve_temp) 334 | @entry.should_not_receive(:[]=) 335 | 336 | lambda { 337 | @entry.avatar_temp = "" 338 | }.should_not change(@entry, :avatar) 339 | end 340 | end 341 | 342 | describe "an upload column with no file" do 343 | 344 | include UploadColumnSpecHelper 345 | 346 | before(:each) do 347 | setup_standard_mocking 348 | end 349 | 350 | it "should return no value" do 351 | @entry.avatar.should be_nil 352 | end 353 | 354 | it "should return no temp_value" do 355 | @entry.avatar_temp.should be_nil 356 | end 357 | 358 | it "should return nothing in the _public_path method" do 359 | @entry.avatar_public_path.should == nil 360 | end 361 | 362 | it "should return nothing in the _path method" do 363 | @entry.avatar_path.should == nil 364 | end 365 | end 366 | 367 | describe "an upload column with an uploaded file" do 368 | 369 | include UploadColumnSpecHelper 370 | 371 | before(:each) do 372 | setup_standard_mocking 373 | UploadColumn::UploadedFile.stub!(:upload).and_return(@uploaded_file) 374 | @entry.avatar = @file 375 | end 376 | 377 | it "should delegate the _public_path method to the column" do 378 | @uploaded_file.should_receive(:public_path).and_return('/url/to/file.exe') 379 | @entry.avatar_public_path.should == '/url/to/file.exe' 380 | end 381 | 382 | it "should delegate the _path method to the column" do 383 | @uploaded_file.should_receive(:path).and_return('/path/to/file.exe') 384 | @entry.avatar_path.should == '/path/to/file.exe' 385 | end 386 | 387 | end 388 | 389 | describe "an upload column with different versions and no uploaded file" do 390 | 391 | include UploadColumnSpecHelper 392 | 393 | before(:each) do 394 | setup_version_mocking # sets up a column with thumb and large versions 395 | end 396 | 397 | it "should return nil for the _thumb method" do 398 | @entry.avatar_thumb.should == nil 399 | end 400 | 401 | it "should return nil for the _large method" do 402 | @entry.avatar_large.should == nil 403 | end 404 | 405 | it "should return nil for the _thumb_url method" do 406 | @entry.avatar_thumb_public_path.should == nil 407 | end 408 | 409 | it "should return nil for the _large_path method" do 410 | @entry.avatar_large_path.should == nil 411 | end 412 | 413 | end 414 | 415 | describe "an upload column with different versions and an uploaded file" do 416 | 417 | include UploadColumnSpecHelper 418 | 419 | before(:each) do 420 | setup_version_mocking # sets up a column with thumb and large versions 421 | UploadColumn::UploadedFile.stub!(:upload).and_return(@uploaded_file) 422 | @entry.avatar = @file 423 | end 424 | 425 | it "should delegate the _thumb method to the column" do 426 | thumb = mock('thumb') 427 | @uploaded_file.should_receive(:thumb).and_return(thumb) 428 | @entry.avatar_thumb.should == thumb 429 | end 430 | 431 | it "should delegate the _large method to the column" do 432 | large = mock('large') 433 | @uploaded_file.should_receive(:large).and_return(large) 434 | @entry.avatar_large.should == large 435 | end 436 | 437 | it "should delegate the _thumb_url method to the column" do 438 | thumb = mock('thumb') 439 | thumb.should_receive(:public_path).and_return('/url/to/file.exe') 440 | @uploaded_file.should_receive(:thumb).and_return(thumb) 441 | 442 | @entry.avatar_thumb_public_path.should == '/url/to/file.exe' 443 | end 444 | 445 | it "should delegate the _large_path method to the column" do 446 | large = mock('large') 447 | large.should_receive(:path).and_return('/path/to/file.exe') 448 | @uploaded_file.should_receive(:large).and_return(large) 449 | 450 | @entry.avatar_large_path.should == '/path/to/file.exe' 451 | end 452 | 453 | end 454 | 455 | describe "uploading a file that fails an integrity check" do 456 | 457 | include UploadColumnSpecHelper 458 | 459 | before(:all) do 460 | Entry.validates_integrity_of :avatar 461 | end 462 | 463 | before(:each) do 464 | setup_standard_mocking 465 | end 466 | 467 | it "should set the column to nil" do 468 | UploadColumn::UploadedFile.should_receive(:upload).and_raise(UploadColumn::IntegrityError.new('something')) 469 | @entry.avatar = @file 470 | 471 | @entry.avatar.should be_nil 472 | end 473 | 474 | it "should fail an integrity validation" do 475 | UploadColumn::UploadedFile.should_receive(:upload).and_raise(UploadColumn::IntegrityError.new('something')) 476 | @entry.avatar = @file 477 | 478 | @entry.should_not be_valid 479 | @entry.errors.on(:avatar).should == 'something' 480 | end 481 | end 482 | 483 | describe UploadColumn::ActiveRecordExtension::ClassMethods, ".image_column" do 484 | 485 | include UploadColumnSpecHelper 486 | 487 | before(:each) do 488 | @class = Class.new(ActiveRecord::Base) 489 | @class.send(:include, UploadColumn) 490 | end 491 | 492 | it "should call an upload column with some specialized options" do 493 | @class.should_receive(:upload_column).with(:sicada, 494 | :manipulator => UploadColumn::Manipulators::RMagick, 495 | :root_dir => File.join(RAILS_ROOT, 'public', 'images'), 496 | :web_root => '/images', 497 | :monkey => 'blah', 498 | :extensions => UploadColumn.image_extensions 499 | ) 500 | @class.image_column(:sicada, :monkey => 'blah') 501 | end 502 | end 503 | 504 | describe UploadColumn::ActiveRecordExtension::ClassMethods, ".validate_integrity" do 505 | 506 | include UploadColumnSpecHelper 507 | 508 | it "should change the options for this upload_column" do 509 | Entry.upload_column :avatar 510 | Entry.reflect_on_upload_columns[:avatar].options[:validate_integrity].should be_nil 511 | Entry.validates_integrity_of :avatar 512 | Entry.reflect_on_upload_columns[:avatar].options[:validate_integrity].should == true 513 | end 514 | end -------------------------------------------------------------------------------- /spec/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | class BeIdenticalWith 2 | def initialize(expected) 3 | @expected = expected 4 | end 5 | def matches?(actual) 6 | @actual = actual 7 | FileUtils.identical?(@actual, @expected) 8 | end 9 | def failure_message 10 | "expected #{@actual.inspect} to be identical with #{@expected.inspect}" 11 | end 12 | def negative_failure_message 13 | "expected #{@actual.inspect} to not be identical with #{@expected.inspect}" 14 | end 15 | end 16 | 17 | def be_identical_with(expected) 18 | BeIdenticalWith.new(expected) 19 | end 20 | 21 | class ExistsPredicate 22 | 23 | def matches?(actual) 24 | actual.exists? 25 | end 26 | def failure_message 27 | "expected #{@actual.inspect} to exist, it doesn't." 28 | end 29 | def negative_failure_message 30 | "expected #{@actual.inspect} to not exist, yet it does." 31 | end 32 | end 33 | 34 | def be_in_existence 35 | ExistsPredicate.new 36 | end 37 | 38 | class MatchPath 39 | def initialize(*expected) 40 | if(expected.size < 2) 41 | @expected = File.expand_path(expected.first) 42 | else 43 | @expected = expected.map {|e| e.is_a?(Regexp) ? e.to_s : Regexp.escape(e)} 44 | @expected = File.expand_path(File.join(*@expected), RAILS_ROOT) 45 | @expected = %r(^#{@expected}$) 46 | end 47 | end 48 | def matches?(actual) 49 | @actual = actual 50 | if @expected.is_a?(Regexp) 51 | File.expand_path(actual) =~ @expected 52 | else 53 | File.expand_path(actual) == @expected 54 | end 55 | end 56 | def failure_message 57 | "expected #{@actual.inspect} to match #{@expected}." 58 | end 59 | def negative_failure_message 60 | "expected #{@actual.inspect} to not match #{@expected}, yet it does." 61 | end 62 | end 63 | 64 | # Match a path without bothering whether they are formatted the same way. 65 | # can also take several parameters, any number of which may be regexes 66 | def match_path(*expected) 67 | MatchPath.new(*expected) 68 | end 69 | 70 | class HavePermissions 71 | def initialize(expected) 72 | @expected = expected 73 | end 74 | 75 | def matches?(actual) 76 | @actual = actual 77 | # Satisfy expectation here. Return false or raise an error if it's not met. 78 | (File.stat(@actual.path).mode & 0777) == @expected 79 | end 80 | 81 | def failure_message 82 | "expected #{@actual.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0777).to_s(8)}" 83 | end 84 | 85 | def negative_failure_message 86 | "expected #{@actual.inspect} not to have permissions #{@expected.to_s(8)}, but it did" 87 | end 88 | end 89 | 90 | def have_permissions(expected) 91 | HavePermissions.new(expected) 92 | end 93 | 94 | class BeNoLargerThan 95 | def initialize(width, height) 96 | @width, @height = width, height 97 | end 98 | 99 | def matches?(actual) 100 | @actual = actual 101 | # Satisfy expectation here. Return false or raise an error if it's not met. 102 | require 'RMagick' 103 | img = ::Magick::Image.read(@actual.path).first 104 | @actual_width = img.columns 105 | @actual_height = img.rows 106 | @actual_width <= @width && @actual_height <= @height 107 | end 108 | 109 | def failure_message 110 | "expected #{@actual.inspect} to be no larger than #{@width} by #{@height}, but it was #{@actual_height} by #{@actual_width}." 111 | end 112 | 113 | def negative_failure_message 114 | "expected #{@actual.inspect} to be larger than #{@width} by #{@height}, but it wasn't." 115 | end 116 | end 117 | 118 | def be_no_larger_than(width, height) 119 | BeNoLargerThan.new(width, height) 120 | end 121 | 122 | class HaveTheExactDimensionsOf 123 | def initialize(width, height) 124 | @width, @height = width, height 125 | end 126 | 127 | def matches?(actual) 128 | @actual = actual 129 | # Satisfy expectation here. Return false or raise an error if it's not met. 130 | require 'RMagick' 131 | img = ::Magick::Image.read(@actual.path).first 132 | @actual_width = img.columns 133 | @actual_height = img.rows 134 | @actual_width == @width && @actual_height == @height 135 | end 136 | 137 | def failure_message 138 | "expected #{@actual.inspect} to have an exact size of #{@width} by #{@height}, but it was #{@actual_height} by #{@actual_width}." 139 | end 140 | 141 | def negative_failure_message 142 | "expected #{@actual.inspect} not to have an exact size of #{@width} by #{@height}, but it did." 143 | end 144 | end 145 | 146 | def have_the_exact_dimensions_of(width, height) 147 | HaveTheExactDimensionsOf.new(width, height) 148 | end -------------------------------------------------------------------------------- /spec/fixtures/animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnicklas/uploadcolumn/c2c14ad15467e97afd905f57d89e05b1b6c65d23/spec/fixtures/animated.gif -------------------------------------------------------------------------------- /spec/fixtures/animated_solarized.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnicklas/uploadcolumn/c2c14ad15467e97afd905f57d89e05b1b6c65d23/spec/fixtures/animated_solarized.gif -------------------------------------------------------------------------------- /spec/fixtures/invalid-image.jpg: -------------------------------------------------------------------------------- 1 | this is certainly not a JPEG image 2 | -------------------------------------------------------------------------------- /spec/fixtures/kerb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnicklas/uploadcolumn/c2c14ad15467e97afd905f57d89e05b1b6c65d23/spec/fixtures/kerb.jpg -------------------------------------------------------------------------------- /spec/fixtures/kerb_solarized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnicklas/uploadcolumn/c2c14ad15467e97afd905f57d89e05b1b6c65d23/spec/fixtures/kerb_solarized.jpg -------------------------------------------------------------------------------- /spec/fixtures/netscape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnicklas/uploadcolumn/c2c14ad15467e97afd905f57d89e05b1b6c65d23/spec/fixtures/netscape.gif -------------------------------------------------------------------------------- /spec/fixtures/skanthak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnicklas/uploadcolumn/c2c14ad15467e97afd905f57d89e05b1b6c65d23/spec/fixtures/skanthak.png -------------------------------------------------------------------------------- /spec/image_science_manipulator_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | require File.join(File.dirname(__FILE__), '../lib/upload_column/manipulators/image_science') 3 | 4 | describe UploadColumn::Manipulators::ImageScience, "#resize!" do 5 | 6 | before(:each) do 7 | @uploaded_file = class << self; self end # this is a singleton object 8 | @uploaded_file.extend( UploadColumn::Manipulators::ImageScience ) 9 | @uploaded_file.load_manipulator_dependencies 10 | @uploaded_file.stub!(:path).and_return('/some_path.png') 11 | end 12 | 13 | it "should preserve the aspect ratio if the image is too wide" do 14 | 15 | image = mock('an image_science object') 16 | 17 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 18 | 19 | image.should_receive(:width).and_return(640) 20 | image.should_receive(:height).and_return(480) 21 | 22 | i2 = mock('another stupid mock') 23 | i2.should_receive(:save).with('/some_path.png') 24 | 25 | image.should_receive(:resize).with(160, 120).and_yield(i2) 26 | 27 | @uploaded_file.resize!('400x120') 28 | end 29 | 30 | it "should preserve the aspect ratio if the image is too narrow" do 31 | 32 | image = mock('an image_science object') 33 | 34 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 35 | 36 | image.should_receive(:width).and_return(640) 37 | image.should_receive(:height).and_return(480) 38 | 39 | i2 = mock('another stupid mock') 40 | i2.should_receive(:save).with('/some_path.png') 41 | 42 | image.should_receive(:resize).with(200, 150).and_yield(i2) 43 | 44 | @uploaded_file.resize!('200x400') 45 | end 46 | 47 | it "should rescale to the exact size if the aspect ratio is the same" do 48 | 49 | image = mock('an image_science object') 50 | 51 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 52 | 53 | image.should_receive(:width).and_return(640) 54 | image.should_receive(:height).and_return(480) 55 | 56 | i2 = mock('another stupid mock') 57 | i2.should_receive(:save).with('/some_path.png') 58 | 59 | image.should_receive(:resize).with(320, 240).and_yield(i2) 60 | 61 | @uploaded_file.resize!('320x240') 62 | end 63 | 64 | it "should not exceed the dimensions if the image is a rather weird size" do 65 | 66 | image = mock('an image_science object') 67 | 68 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 69 | 70 | image.should_receive(:width).and_return(737) 71 | image.should_receive(:height).and_return(237) 72 | 73 | i2 = mock('another stupid mock') 74 | i2.should_receive(:save).with('/some_path.png') 75 | 76 | image.should_receive(:resize).with(137, 44).and_yield(i2) 77 | 78 | @uploaded_file.resize!('137x137') 79 | end 80 | 81 | end 82 | 83 | 84 | describe UploadColumn::Manipulators::ImageScience, "#crop_resized!" do 85 | 86 | before(:each) do 87 | @uploaded_file = class << self; self end # this is a singleton object 88 | @uploaded_file.extend( UploadColumn::Manipulators::ImageScience ) 89 | @uploaded_file.load_manipulator_dependencies 90 | @uploaded_file.stub!(:path).and_return('/some_path.png') 91 | end 92 | 93 | it "should crop and resize an image that is too tall" do 94 | image = mock('an image_science object') 95 | 96 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 97 | 98 | image.should_receive(:width).and_return(640) 99 | image.should_receive(:height).and_return(480) 100 | 101 | i2 = mock('another stupid mock') 102 | image.should_receive(:resize).with(400, 300).and_yield(i2) 103 | 104 | i3 = mock('image science is stupid') 105 | i2.should_receive(:with_crop).with(0, 90, 400, 210).and_yield(i3) 106 | 107 | i3.should_receive(:save).with('/some_path.png') 108 | 109 | @uploaded_file.crop_resized!('400x120') 110 | end 111 | 112 | it "should crop and resize an image that is too tall" do 113 | image = mock('an image_science object') 114 | 115 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 116 | 117 | image.should_receive(:width).and_return(640) 118 | image.should_receive(:height).and_return(480) 119 | 120 | i2 = mock('another stupid mock') 121 | image.should_receive(:resize).with(560, 420).and_yield(i2) 122 | 123 | i3 = mock('image science is stupid') 124 | i2.should_receive(:with_crop).with(180, 0, 380, 420).and_yield(i3) 125 | 126 | i3.should_receive(:save).with('/some_path.png') 127 | 128 | @uploaded_file.crop_resized!('200x420') 129 | end 130 | 131 | it "should crop and resize an image with the correct aspect ratio" do 132 | image = mock('an image_science object') 133 | 134 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 135 | 136 | image.should_receive(:width).and_return(640) 137 | image.should_receive(:height).and_return(480) 138 | 139 | i2 = mock('another stupid mock') 140 | image.should_receive(:resize).with(320, 240).and_yield(i2) 141 | 142 | i3 = mock('image science is stupid') 143 | i2.should_receive(:with_crop).with(0, 0, 320, 240).and_yield(i3) 144 | 145 | i3.should_receive(:save).with('/some_path.png') 146 | 147 | @uploaded_file.crop_resized!('320x240') 148 | end 149 | 150 | it "should crop and resize an image with weird dimensions" do 151 | image = mock('an image_science object') 152 | 153 | ::ImageScience.should_receive(:with_image).with('/some_path.png').and_yield(image) 154 | 155 | image.should_receive(:width).and_return(737) 156 | image.should_receive(:height).and_return(967) 157 | 158 | i2 = mock('another stupid mock') 159 | image.should_receive(:resize).with(333, 437).and_yield(i2) 160 | 161 | i3 = mock('image science is stupid') 162 | i2.should_receive(:with_crop).with(0, 150, 333, 287).and_yield(i3) 163 | 164 | i3.should_receive(:save).with('/some_path.png') 165 | 166 | @uploaded_file.crop_resized!('333x137') 167 | end 168 | end 169 | 170 | describe UploadColumn::Manipulators::ImageScience, "#process!" do 171 | 172 | before(:each) do 173 | @uploaded_file = class << self; self end 174 | @uploaded_file.extend( UploadColumn::Manipulators::ImageScience ) 175 | @uploaded_file.load_manipulator_dependencies 176 | @uploaded_file.stub!(:path).and_return('/some_path.png') 177 | end 178 | 179 | it "should resize the image if a string like '333x444' is passed" do 180 | @uploaded_file.should_receive(:resize!).with('333x444') 181 | @uploaded_file.process!('333x444') 182 | end 183 | 184 | it "should crop and resize the image if a string like 'c333x444' is passed" do 185 | @uploaded_file.should_receive(:crop_resized!).with('333x444') 186 | @uploaded_file.process!('c333x444') 187 | end 188 | 189 | it "should do nothing if :none is passed" do 190 | @uploaded_file.should_not_receive(:manipulate!) 191 | @uploaded_file.process!(:none) 192 | end 193 | 194 | end 195 | 196 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | gem 'activerecord' 4 | require 'active_record' 5 | 6 | require File.join(File.dirname(__FILE__), '../lib/upload_column') 7 | 8 | # change this if sqlite is unavailable 9 | dbconfig = { 10 | :adapter => 'sqlite3', 11 | :database => 'db/test.sqlite3' 12 | } 13 | 14 | ActiveRecord::Base.establish_connection(dbconfig) 15 | ActiveRecord::Migration.verbose = false 16 | 17 | class TestMigration < ActiveRecord::Migration 18 | def self.up 19 | create_table :events, :force => true do |t| 20 | t.column :image, :string 21 | t.column :textfile, :string 22 | end 23 | 24 | create_table :movies, :force => true do |t| 25 | t.column :movie, :string 26 | t.column :name, :string 27 | t.column :description, :text 28 | end 29 | 30 | create_table :shrooms, :force => true do |t| 31 | t.column :image, :string 32 | t.column :image_size, :integer 33 | t.column :image_public_path, :string 34 | t.column :image_path, :string 35 | t.column :image_monkey, :string 36 | t.column :image_extension, :string 37 | end 38 | end 39 | 40 | def self.down 41 | drop_table :events 42 | drop_table :movies 43 | drop_table :shrooms 44 | end 45 | end 46 | 47 | class Event < ActiveRecord::Base; end # setup a basic AR class for testing 48 | class Movie < ActiveRecord::Base; end # setup a basic AR class for testing 49 | class Shroom < ActiveRecord::Base; end # setup a basic AR class for testing 50 | 51 | def migrate 52 | before(:all) do 53 | TestMigration.down rescue nil 54 | TestMigration.up 55 | end 56 | 57 | after(:all) { TestMigration.down } 58 | end 59 | 60 | # TODO: RSpec syntax and integration really don't mix. In the long run, it would 61 | # be nice to rewrite this stuff with the Story runner. 62 | 63 | describe "normally instantiating and saving a record" do 64 | 65 | migrate 66 | 67 | it "shouldn't fail" do 68 | Event.reflect_on_upload_columns.should == {} 69 | running { @event = Event.new }.should_not raise_error 70 | @event.image = "monkey" 71 | running { @event.save }.should_not raise_error 72 | end 73 | 74 | end 75 | 76 | describe "uploading a single file" do 77 | 78 | migrate 79 | 80 | before do 81 | Event.upload_column(:image) 82 | @event = Event.new 83 | @event.image = stub_tempfile('kerb.jpg') 84 | end 85 | 86 | it "should set the correct path" do 87 | @event.image.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb.jpg' ) 88 | end 89 | 90 | it "should copy the file to temp." do 91 | File.exists?(@event.image.path).should === true 92 | @event.image.path.should be_identical_with(file_path('kerb.jpg')) 93 | end 94 | 95 | it "should set the correct url" do 96 | @event.image.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb.jpg} 97 | end 98 | 99 | after do 100 | FileUtils.rm_rf(PUBLIC) 101 | end 102 | end 103 | 104 | describe "uploading a file and then saving the record" do 105 | 106 | migrate 107 | 108 | before do 109 | Event.upload_column(:image) 110 | @event = Event.new 111 | @event.image = stub_tempfile('kerb.jpg') 112 | @event.save 113 | end 114 | 115 | it "should set the correct path" do 116 | @event.image.path.should match_path(PUBLIC, 'image', 'kerb.jpg') 117 | end 118 | 119 | it "should copy the file to the correct location" do 120 | File.exists?(@event.image.path) 121 | @event.image.path.should be_identical_with(file_path('kerb.jpg')) 122 | end 123 | 124 | it "should set the correct url" do 125 | @event.image.url.should == "/image/kerb.jpg" 126 | end 127 | 128 | it "should save the filename to the database" do 129 | Event.find(@event.id)['image'].should == 'kerb.jpg' 130 | end 131 | 132 | after do 133 | FileUtils.rm_rf(PUBLIC) 134 | end 135 | 136 | end 137 | 138 | describe "uploading a file with versions" do 139 | 140 | migrate 141 | 142 | before do 143 | Event.upload_column(:image, :versions => [ :thumb, :large ] ) 144 | @event = Event.new 145 | @event.image = stub_tempfile('kerb.jpg') 146 | end 147 | 148 | it "should set the correct path" do 149 | @event.image.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb.jpg' ) 150 | @event.image.thumb.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb-thumb.jpg' ) 151 | @event.image.large.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb-large.jpg' ) 152 | end 153 | 154 | it "should copy the file to temp." do 155 | File.exists?(@event.image.path).should === true 156 | File.exists?(@event.image.thumb.path).should === true 157 | File.exists?(@event.image.large.path).should === true 158 | @event.image.path.should be_identical_with(file_path('kerb.jpg')) 159 | @event.image.thumb.path.should be_identical_with(file_path('kerb.jpg')) 160 | @event.image.large.path.should be_identical_with(file_path('kerb.jpg')) 161 | end 162 | 163 | it "should set the correct url" do 164 | @event.image.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb.jpg} 165 | @event.image.thumb.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb-thumb.jpg} 166 | @event.image.large.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb-large.jpg} 167 | end 168 | 169 | after do 170 | FileUtils.rm_rf(PUBLIC) 171 | end 172 | 173 | end 174 | 175 | describe "uploading a file with versions and then saving the record" do 176 | 177 | migrate 178 | 179 | before do 180 | Event.upload_column(:image, :versions => [ :thumb, :large ] ) 181 | @event = Event.new 182 | @event.image = stub_tempfile('kerb.jpg') 183 | @event.save 184 | end 185 | 186 | it "should set the correct path" do 187 | @event.image.path.should match_path(PUBLIC, 'image', 'kerb.jpg' ) 188 | @event.image.thumb.path.should match_path(PUBLIC, 'image', 'kerb-thumb.jpg' ) 189 | @event.image.large.path.should match_path(PUBLIC, 'image', 'kerb-large.jpg' ) 190 | end 191 | 192 | it "should copy the file to the correct location." do 193 | File.exists?(@event.image.path).should === true 194 | File.exists?(@event.image.thumb.path).should === true 195 | File.exists?(@event.image.large.path).should === true 196 | @event.image.path.should be_identical_with(file_path('kerb.jpg')) 197 | @event.image.thumb.path.should be_identical_with(file_path('kerb.jpg')) 198 | @event.image.large.path.should be_identical_with(file_path('kerb.jpg')) 199 | end 200 | 201 | it "should set the correct url" do 202 | @event.image.url.should == "/image/kerb.jpg" 203 | @event.image.thumb.url.should == "/image/kerb-thumb.jpg" 204 | @event.image.large.url.should == "/image/kerb-large.jpg" 205 | end 206 | 207 | it "should save the filename to the database" do 208 | Event.find(@event.id)['image'].should == 'kerb.jpg' 209 | end 210 | 211 | after do 212 | FileUtils.rm_rf(PUBLIC) 213 | end 214 | 215 | end 216 | 217 | 218 | describe "assigning a file from temp with versions" do 219 | 220 | migrate 221 | 222 | before do 223 | Event.upload_column(:image, :versions => [ :thumb, :large ] ) 224 | @blah = Event.new 225 | 226 | @event = Event.new 227 | 228 | @blah.image = stub_tempfile('kerb.jpg') # we've alredy tested this... 229 | 230 | @event.image_temp = @blah.image_temp 231 | end 232 | 233 | it "should set the correct path" do 234 | @event.image.path.should == @blah.image.path 235 | @event.image.thumb.path.should == @blah.image.thumb.path 236 | @event.image.large.path.should == @blah.image.large.path 237 | end 238 | 239 | it "should set the correct url" do 240 | @event.image.url.should == @blah.image.url 241 | @event.image.thumb.url.should == @blah.image.thumb.url 242 | @event.image.large.url.should == @blah.image.large.url 243 | end 244 | 245 | after do 246 | FileUtils.rm_rf(PUBLIC) 247 | end 248 | 249 | end 250 | 251 | 252 | describe "assigning a file from temp with versions and then saving the record" do 253 | 254 | migrate 255 | 256 | before do 257 | Event.upload_column(:image, :versions => [ :thumb, :large ] ) 258 | @blah = Event.new 259 | 260 | @event = Event.new 261 | 262 | @blah.image = stub_tempfile('kerb.jpg') # we've alredy tested this... 263 | 264 | @event.image_temp = @blah.image_temp 265 | 266 | @event.save 267 | end 268 | 269 | it "should set the correct path" do 270 | @event.image.path.should match_path(PUBLIC, 'image', 'kerb.jpg' ) 271 | @event.image.thumb.path.should match_path(PUBLIC, 'image', 'kerb-thumb.jpg' ) 272 | @event.image.large.path.should match_path(PUBLIC, 'image', 'kerb-large.jpg' ) 273 | end 274 | 275 | it "should copy the file to the correct location." do 276 | File.exists?(@event.image.path).should === true 277 | File.exists?(@event.image.thumb.path).should === true 278 | File.exists?(@event.image.large.path).should === true 279 | @event.image.path.should be_identical_with(file_path('kerb.jpg')) 280 | @event.image.thumb.path.should be_identical_with(file_path('kerb.jpg')) 281 | @event.image.large.path.should be_identical_with(file_path('kerb.jpg')) 282 | end 283 | 284 | it "should set the correct url" do 285 | @event.image.url.should == "/image/kerb.jpg" 286 | @event.image.thumb.url.should == "/image/kerb-thumb.jpg" 287 | @event.image.large.url.should == "/image/kerb-large.jpg" 288 | end 289 | 290 | it "should save the filename to the database" do 291 | Event.find(@event.id)['image'].should == 'kerb.jpg' 292 | end 293 | 294 | after do 295 | FileUtils.rm_rf(PUBLIC) 296 | end 297 | 298 | end 299 | 300 | describe "an upload_column with an uploaded file" do 301 | 302 | migrate 303 | 304 | before do 305 | Event.upload_column(:image) 306 | @event = Event.new 307 | @event.image = stub_tempfile('kerb.jpg') 308 | @event.save 309 | end 310 | 311 | it "should not be overwritten by an empty String" do 312 | @e2 = Event.find(@event.id) 313 | lambda { 314 | @e2.image = "" 315 | @e2.save 316 | }.should_not change(@e2.image, :path) 317 | @e2[:image].should == "kerb.jpg" 318 | end 319 | 320 | it "should not be overwritten by an empty StringIO" do 321 | @e2 = Event.find(@event.id) 322 | lambda { 323 | @e2.image = StringIO.new('') 324 | @e2.save 325 | }.should_not change(@e2.image, :path) 326 | @e2[:image].should == "kerb.jpg" 327 | end 328 | 329 | it "should not be overwritten by an empty file" do 330 | @e2 = Event.find(@event.id) 331 | lambda { 332 | file = stub_file('kerb.jpg') 333 | file.stub!(:size).and_return(0) 334 | @e2.image = file 335 | @e2.save 336 | }.should_not change(@e2.image, :path) 337 | @e2[:image].should == "kerb.jpg" 338 | end 339 | 340 | it "should be overwritten by another file" do 341 | @e2 = Event.find(@event.id) 342 | lambda { 343 | file = stub_file('skanthak.png') 344 | @e2.image = file 345 | @e2.save 346 | }.should_not change(@e2.image, :path) 347 | @e2[:image].should == "skanthak.png" 348 | end 349 | 350 | it "should be marshallable" do 351 | running { Marshal.dump(@entry) }.should_not raise_error 352 | end 353 | 354 | after do 355 | FileUtils.rm_rf(PUBLIC) 356 | end 357 | end 358 | 359 | describe "uploading an image with several versions, the rmagick manipulator and instructions to rescale" do 360 | 361 | migrate 362 | 363 | # buuhuu so sue me. This spec runs a whole second faster if we do this before all instead of 364 | # before each. 365 | before(:all) do 366 | Event.upload_column(:image, 367 | :versions => { :thumb => 'c100x100', :large => '200x200' }, 368 | :manipulator => UploadColumn::Manipulators::RMagick 369 | ) 370 | @event = Event.new 371 | @event.image = stub_tempfile('kerb.jpg') 372 | end 373 | 374 | it "should set the correct path" do 375 | @event.image.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb.jpg' ) 376 | @event.image.thumb.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb-thumb.jpg' ) 377 | @event.image.large.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb-large.jpg' ) 378 | end 379 | 380 | it "should copy the files to temp." do 381 | File.exists?(@event.image.path).should === true 382 | File.exists?(@event.image.thumb.path).should === true 383 | File.exists?(@event.image.large.path).should === true 384 | end 385 | 386 | it "should set the correct url" do 387 | @event.image.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb.jpg} 388 | @event.image.thumb.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb-thumb.jpg} 389 | @event.image.large.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb-large.jpg} 390 | end 391 | 392 | it "should preserve the main file" do 393 | @event.image.path.should be_identical_with(file_path('kerb.jpg')) 394 | end 395 | 396 | it "should change the versions" do 397 | @event.image.thumb.path.should_not be_identical_with(file_path('kerb.jpg')) 398 | @event.image.large.path.should_not be_identical_with(file_path('kerb.jpg')) 399 | end 400 | 401 | it "should rescale the images to the correct sizes" do 402 | @event.image.large.should be_no_larger_than(200, 200) 403 | @event.image.thumb.should have_the_exact_dimensions_of(100, 100) 404 | end 405 | 406 | after(:all) do 407 | FileUtils.rm_rf(PUBLIC) 408 | end 409 | end 410 | 411 | 412 | # TODO: make image_science not crap out on my macbook 413 | #describe "uploading an image with several versions, the image_science manipulator and instructions to rescale" do 414 | # 415 | # migrate 416 | # 417 | # # buuhuu so sue me. This spec runs a whole second faster if we do this before all instead of 418 | # # before each. 419 | # before(:all) do 420 | # Event.upload_column(:image, 421 | # :versions => { :thumb => 'c100x100', :large => '200x200' }, 422 | # :manipulator => UploadColumn::Manipulators::ImageScience 423 | # ) 424 | # @event = Event.new 425 | # @event.image = stub_tempfile('kerb.jpg') 426 | # end 427 | # 428 | # it "should set the correct path" do 429 | # @event.image.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb.jpg' ) 430 | # @event.image.thumb.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb-thumb.jpg' ) 431 | # @event.image.large.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'kerb-large.jpg' ) 432 | # end 433 | # 434 | # it "should copy the files to temp." do 435 | # File.exists?(@event.image.path).should === true 436 | # File.exists?(@event.image.thumb.path).should === true 437 | # File.exists?(@event.image.large.path).should === true 438 | # end 439 | # 440 | # it "should set the correct url" do 441 | # @event.image.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb.jpg} 442 | # @event.image.thumb.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb-thumb.jpg} 443 | # @event.image.large.url.should =~ %r{/tmp/(?:\d+\.)+\d+/kerb-large.jpg} 444 | # end 445 | # 446 | # it "should preserve the main file" do 447 | # @event.image.path.should be_identical_with(file_path('kerb.jpg')) 448 | # end 449 | # 450 | # it "should change the versions" do 451 | # @event.image.thumb.path.should_not be_identical_with(file_path('kerb.jpg')) 452 | # @event.image.large.path.should_not be_identical_with(file_path('kerb.jpg')) 453 | # end 454 | # 455 | # it "should rescale the images to the correct sizes" do 456 | # @event.image.large.should be_no_larger_than(200, 200) 457 | # @event.image.thumb.should have_the_exact_dimensions_of(100, 100) 458 | # end 459 | # 460 | # after(:all) do 461 | # FileUtils.rm_rf(PUBLIC) 462 | # end 463 | #end 464 | 465 | describe "uploading a file with an extension that is not in the whitelist" do 466 | 467 | migrate 468 | 469 | before(:each) do 470 | Event.upload_column(:image, :fix_file_extensions => false) 471 | Event.validates_integrity_of :image 472 | 473 | @event = Event.new 474 | end 475 | 476 | it "should add an error to the record" do 477 | @event.image = stub_tempfile('kerb.jpg', nil, 'monkey.exe') 478 | @event.should_not be_valid 479 | @event.errors.on(:image).should == "has an extension that is not allowed." 480 | @event.image.should be_nil 481 | end 482 | 483 | it "should be reversible by uploading a valid file" do 484 | 485 | @event.image = stub_tempfile('kerb.jpg', nil, 'monkey.exe') 486 | 487 | @event.should_not be_valid 488 | @event.errors.on(:image).should include('has an extension that is not allowed.') 489 | 490 | @event.image = stub_tempfile('kerb.jpg') 491 | 492 | @event.should be_valid 493 | @event.errors.on(:image).should be_nil 494 | end 495 | end 496 | 497 | describe "uploading a file with magic columns" do 498 | 499 | migrate 500 | 501 | before(:each) do 502 | Shroom.upload_column :image 503 | @shroom = Shroom.new 504 | @shroom.image = stub_tempfile('kerb.jpg') 505 | end 506 | 507 | it "should automatically set the image path" do 508 | @shroom.image_path.should == @shroom.image.path 509 | end 510 | 511 | it "should automatically set the image size" do 512 | @shroom.image_size.should == @shroom.image.size 513 | end 514 | 515 | it "should automatically set the image public path" do 516 | @shroom.image_public_path.should == @shroom.image.public_path 517 | end 518 | 519 | it "should ignore columns whose names aren't methods on the column" do 520 | @shroom.image_monkey.should == nil 521 | end 522 | end 523 | 524 | describe "assigning a file from tmp with magic columns" do 525 | 526 | migrate 527 | 528 | before(:each) do 529 | Shroom.upload_column :image 530 | e1 = Shroom.new 531 | e1.image = stub_tempfile('kerb.jpg') 532 | @shroom = Shroom.new 533 | @shroom.image_temp = e1.image_temp 534 | end 535 | 536 | it "should automatically set the image size" do 537 | @shroom.image_size.should == @shroom.image.size 538 | end 539 | 540 | it "should automatically set the image path" do 541 | @shroom.image_path.should == @shroom.image.path 542 | end 543 | 544 | it "should automatically set the image public path" do 545 | @shroom.image_public_path.should == @shroom.image.public_path 546 | end 547 | 548 | it "should ignore columns whose names aren't methods on the column" do 549 | @shroom.image_monkey.should == nil 550 | end 551 | end 552 | 553 | describe "uploading and saving a file with magic columns" do 554 | 555 | migrate 556 | 557 | before(:each) do 558 | Shroom.upload_column :image 559 | @shroom = Shroom.new 560 | @shroom.image_extension = "Some other Extension" 561 | @shroom.image = stub_tempfile('kerb.jpg') 562 | @shroom.save 563 | end 564 | 565 | it "should automatically set the image size" do 566 | @shroom.image_size.should == @shroom.image.size 567 | end 568 | 569 | it "should automatically set the image path" do 570 | @shroom.image_path.should == @shroom.image.path 571 | end 572 | 573 | it "should automatically set the image public path" do 574 | @shroom.image_public_path.should == @shroom.image.public_path 575 | end 576 | 577 | it "should ignore columns whose names aren't methods on the column" do 578 | @shroom.image_monkey.should == nil 579 | end 580 | 581 | it "should ignore columns who already have a value set" do 582 | @shroom.image_extension.should == "Some other Extension" 583 | end 584 | end 585 | 586 | describe "assigning a file from tmp and saving it with magic columns" do 587 | 588 | migrate 589 | 590 | before(:each) do 591 | Shroom.upload_column :image 592 | e1 = Shroom.new 593 | e1.image = stub_tempfile('kerb.jpg') 594 | @shroom = Shroom.new 595 | @shroom.image_temp = e1.image_temp 596 | @shroom.save 597 | end 598 | 599 | it "should automatically set the image size" do 600 | @shroom.image_size.should == @shroom.image.size 601 | end 602 | 603 | it "should automatically set the image path" do 604 | @shroom.image_path.should == @shroom.image.path 605 | end 606 | 607 | it "should automatically set the image public path" do 608 | @shroom.image_public_path.should == @shroom.image.public_path 609 | end 610 | 611 | it "should ignore columns whose names aren't methods on the column" do 612 | @shroom.image_monkey.should == nil 613 | end 614 | end 615 | 616 | describe "uploading a file with a filename instruction" do 617 | 618 | migrate 619 | 620 | before(:each) do 621 | Event.upload_column :image, :filename => 'arg.png' 622 | @event = Event.new 623 | @event.image = stub_tempfile('kerb.jpg') 624 | @event.save 625 | end 626 | 627 | it "should give it the correct filename" do 628 | @event.image.filename.should == 'arg.png' 629 | end 630 | 631 | it "should give it the correct path" do 632 | @event.image.path.should match_path(PUBLIC, 'image', 'arg.png') 633 | end 634 | end 635 | 636 | describe "uploading a file with a complex filename instruction" do 637 | 638 | migrate 639 | 640 | before(:each) do 641 | Movie.upload_column :image, :filename => proc{ |r, f| "#{r.name}-#{f.basename}-#{f.suffix}quox.#{f.extension}"}, :versions => [:thumb, :large] 642 | @movie = Movie.new 643 | @movie.name = "indiana_jones" 644 | @movie.image = stub_tempfile('kerb.jpg') 645 | @movie.save 646 | end 647 | 648 | it "should give it the correct filename" do 649 | @movie.image.filename.should == 'indiana_jones-kerb-quox.jpg' 650 | @movie.image.thumb.filename.should == 'indiana_jones-kerb-thumbquox.jpg' 651 | @movie.image.large.filename.should == 'indiana_jones-kerb-largequox.jpg' 652 | end 653 | 654 | it "should have correct paths" do 655 | @movie.image.path.should match_path(PUBLIC, 'image', 'indiana_jones-kerb-quox.jpg' ) 656 | @movie.image.thumb.path.should match_path(PUBLIC, 'image', 'indiana_jones-kerb-thumbquox.jpg' ) 657 | @movie.image.large.path.should match_path(PUBLIC, 'image', 'indiana_jones-kerb-largequox.jpg' ) 658 | end 659 | 660 | it "should remember the original filename" do 661 | @movie.image.actual_filename.should == "kerb.jpg" 662 | end 663 | 664 | it "should store the _original_ filename in the database" do 665 | @movie[:image].should == "kerb.jpg" 666 | end 667 | 668 | end -------------------------------------------------------------------------------- /spec/magic_columns_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | gem 'activerecord' 4 | require 'active_record' 5 | 6 | require File.join(File.dirname(__FILE__), '../lib/upload_column') 7 | 8 | ActiveRecord::Base.send(:include, UploadColumn) 9 | 10 | class Entry < ActiveRecord::Base; end # setup a basic AR class for testing 11 | 12 | describe "UploadColumn::MagicColumns.set_upload_column_with_magic_columns" do 13 | 14 | include UploadColumnSpecHelper 15 | 16 | before(:each) do 17 | setup_standard_mocking 18 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 19 | end 20 | 21 | it "should assign methods from the uploaded file to database columns" do 22 | Entry.should_receive(:column_names).and_return([ 'monkey', 'llama', 'avatar_path', 'avatar_size']) 23 | 24 | @uploaded_file.stub!(:path).and_return('/path/to/my/file') 25 | @uploaded_file.stub!(:size).and_return(9999) 26 | 27 | @entry.avatar = @file 28 | 29 | @entry.avatar_path.should == '/path/to/my/file' 30 | @entry.avatar_size.should == 9999 31 | end 32 | 33 | it "should do nothing when the column names do not exist on the object" do 34 | Entry.should_receive(:column_names).and_return([ 'monkey', 'llama', 'avatar_monkey', 'avatar_size']) 35 | 36 | @uploaded_file.stub!(:size).and_return(9999) 37 | 38 | @entry.avatar = @file 39 | 40 | @entry.avatar_monkey.should be_nil 41 | @entry.avatar_size.should == 9999 42 | end 43 | end 44 | 45 | describe "UploadColumn::MagicColumns.set_upload_column_temp_with_magic_columns" do 46 | 47 | include UploadColumnSpecHelper 48 | 49 | before(:each) do 50 | setup_standard_mocking 51 | 52 | @temp_value = '12345.1234.12345/somewhere.png' 53 | 54 | @retrieved_file = mock('a retrieved file') 55 | @retrieved_file.should_receive(:actual_filename).and_return('walruss.png') 56 | 57 | UploadColumn::UploadedFile.should_receive(:retrieve_temp).with(@temp_value, @entry, :avatar, @options).and_return(@retrieved_file) 58 | @entry.should_receive(:[]=).with(:avatar, 'walruss.png') 59 | end 60 | 61 | it "should assign methods from the uploaded file to database columns" do 62 | Entry.stub!(:column_names).and_return([ 'monkey', 'llama', 'avatar_path', 'avatar_size']) 63 | 64 | @retrieved_file.stub!(:path).and_return('/path/to/my/file') 65 | @retrieved_file.stub!(:size).and_return(9999) 66 | 67 | @entry.avatar_temp = @temp_value 68 | 69 | @entry.avatar_path.should == '/path/to/my/file' 70 | @entry.avatar_size.should == 9999 71 | end 72 | 73 | it "should do nothing when the column names do not exist on the object" do 74 | Entry.stub!(:column_names).and_return([ 'monkey', 'llama', 'avatar_monkey', 'avatar_size']) 75 | 76 | @retrieved_file.stub!(:size).and_return(9999) 77 | 78 | @entry.avatar_temp = @temp_value 79 | 80 | @entry.avatar_monkey.should be_nil 81 | @entry.avatar_size.should == 9999 82 | end 83 | end 84 | 85 | describe "UploadColumn::MagicColumns.save_uploaded_files_with_magic_columns" do 86 | 87 | include UploadColumnSpecHelper 88 | 89 | before(:each) do 90 | setup_standard_mocking 91 | UploadColumn::UploadedFile.should_receive(:upload).with(@file, @entry, :avatar, @options).and_return(@uploaded_file) 92 | @entry.avatar = @file 93 | @uploaded_file.stub!(:tempfile?).and_return(false) 94 | end 95 | 96 | it "should reevaluate magic columns" do 97 | Entry.stub!(:column_names).and_return([ 'monkey', 'llama', 'avatar_path', 'avatar_size']) 98 | 99 | @uploaded_file.stub!(:path).and_return('/path/to/my/file') 100 | @uploaded_file.stub!(:size).and_return(9999) 101 | 102 | @entry.send(:save_uploaded_files) 103 | 104 | @entry.avatar_path.should == '/path/to/my/file' 105 | @entry.avatar_size.should == 9999 106 | end 107 | 108 | it "should do nothing when the column names do not exist on the object" do 109 | Entry.stub!(:column_names).and_return([ 'monkey', 'llama', 'avatar_monkey', 'avatar_size']) 110 | 111 | @uploaded_file.stub!(:size).and_return(9999) 112 | 113 | @entry.send(:save_uploaded_files) 114 | 115 | @entry.avatar_monkey.should be_nil 116 | @entry.avatar_size.should == 9999 117 | end 118 | 119 | end 120 | 121 | -------------------------------------------------------------------------------- /spec/rmagick_manipulator_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | require File.join(File.dirname(__FILE__), '../lib/upload_column/manipulators/rmagick') 3 | 4 | describe UploadColumn::Manipulators::RMagick, "#manipulate!" do 5 | 6 | before(:each) do 7 | @uploaded_file = class << self; self end # this is a singleton object 8 | @uploaded_file.extend( UploadColumn::Manipulators::RMagick ) 9 | @uploaded_file.load_manipulator_dependencies 10 | @uploaded_file.stub!(:path).and_return('/some_path.png') 11 | end 12 | 13 | it "should yield the first frame of the image and then save the result, for a single-framed image" do 14 | a_frame = mock('a frame') 15 | Magick::Image.should_receive(:read).with('/some_path.png').and_return( [a_frame] ) 16 | 17 | @uploaded_file.manipulate! do |img| 18 | img.should == a_frame 19 | img.should_receive(:write).with('/some_path.png') 20 | img 21 | end 22 | end 23 | 24 | it "should yield all frames and save the result, for a multi-framed image" do 25 | image = Magick::Image.read(file_path('netscape.gif')) 26 | Magick::Image.should_receive(:read).with('/some_path.png').and_return( image ) 27 | 28 | imagelist = Magick::ImageList.new 29 | Magick::ImageList.should_receive(:new).and_return(imagelist) 30 | 31 | imagelist.should_receive(:<<).with(image[0]).exactly(:once).ordered 32 | imagelist.should_receive(:<<).with(image[1]).exactly(:once).ordered 33 | 34 | image[0].should_receive(:solarize) 35 | image[1].should_receive(:solarize) 36 | 37 | imagelist.should_receive(:write).with('/some_path.png') 38 | 39 | @uploaded_file.manipulate! do |img| 40 | img.solarize 41 | img 42 | end 43 | 44 | end 45 | 46 | it "should raise an more meaningful error if something goes wrong" do 47 | Magick::Image.should_receive(:read).and_raise(Magick::ImageMagickError.new('arrggh')) 48 | 49 | lambda do 50 | @uploaded_file.manipulate! do |img| 51 | img 52 | end 53 | end.should raise_error( UploadColumn::ManipulationError, "Failed to manipulate with rmagick, maybe it is not an image? Original Error: arrggh" ) 54 | 55 | end 56 | 57 | end 58 | 59 | describe UploadColumn::Manipulators::RMagick, "#resize!" do 60 | 61 | before(:each) do 62 | @uploaded_file = class << self; self end 63 | @uploaded_file.extend( UploadColumn::Manipulators::RMagick ) 64 | @uploaded_file.load_manipulator_dependencies 65 | @uploaded_file.stub!(:path).and_return('/some_path.png') 66 | end 67 | 68 | it "should use rmagick to resize the image to the appropriate size" do 69 | 70 | img = mock('an image frame') 71 | @uploaded_file.should_receive(:manipulate!).and_yield(img) 72 | 73 | geometry_img = mock('image returned by change_geometry') 74 | 75 | img.should_receive(:change_geometry).with("200x200").and_yield(20, 40, geometry_img) 76 | 77 | geometry_img.should_receive(:resize).with(20, 40) 78 | 79 | @uploaded_file.resize!("200x200") 80 | end 81 | 82 | end 83 | 84 | 85 | describe UploadColumn::Manipulators::RMagick, "#crop_resized!" do 86 | 87 | before(:each) do 88 | @uploaded_file = class << self; self end 89 | @uploaded_file.extend( UploadColumn::Manipulators::RMagick ) 90 | @uploaded_file.load_manipulator_dependencies 91 | @uploaded_file.stub!(:path).and_return('/some_path.png') 92 | end 93 | 94 | it "should use rmagick to resize and crop the image to the appropriate size" do 95 | 96 | img = mock('an image frame') 97 | @uploaded_file.should_receive(:manipulate!).and_yield(img) 98 | 99 | img.should_receive(:crop_resized).with(200, 200) 100 | 101 | @uploaded_file.crop_resized!("200x200") 102 | end 103 | 104 | end 105 | 106 | describe UploadColumn::Manipulators::RMagick, "#convert!" do 107 | 108 | before(:each) do 109 | @uploaded_file = class << self; self end 110 | @uploaded_file.extend( UploadColumn::Manipulators::RMagick ) 111 | @uploaded_file.load_manipulator_dependencies 112 | @uploaded_file.stub!(:path).and_return('/some_path.png') 113 | end 114 | 115 | it "should use rmagick to change the image format" do 116 | 117 | img = mock('an image frame') 118 | @uploaded_file.should_receive(:manipulate!).and_yield(img) 119 | 120 | img.should_receive(:format=).with("PNG") 121 | 122 | @uploaded_file.convert!(:png) 123 | end 124 | 125 | end 126 | 127 | describe UploadColumn::Manipulators::RMagick, "#process!" do 128 | 129 | before(:each) do 130 | @uploaded_file = class << self; self end 131 | @uploaded_file.extend( UploadColumn::Manipulators::RMagick ) 132 | @uploaded_file.load_manipulator_dependencies 133 | @uploaded_file.stub!(:path).and_return('/some_path.png') 134 | end 135 | 136 | it "should resize the image if a string like '333x444' is passed" do 137 | @uploaded_file.should_receive(:resize!).with('333x444') 138 | @uploaded_file.process!('333x444') 139 | end 140 | 141 | it "should crop and resize the image if a string like 'c333x444' is passed" do 142 | @uploaded_file.should_receive(:crop_resized!).with('333x444') 143 | @uploaded_file.process!('c333x444') 144 | end 145 | 146 | it "should pass on a proc to manipulate!" do 147 | img_frame = mock('an image frame') 148 | proc = proc { |img| img.solarize } 149 | img_frame.should_receive(:solarize) 150 | 151 | @uploaded_file.should_receive(:manipulate!).and_yield(img_frame) 152 | 153 | @uploaded_file.process!(proc) 154 | end 155 | 156 | it "should yield to manipulate! if a block is given" do 157 | img_frame = mock('an image frame') 158 | img_frame.should_receive(:solarize) 159 | 160 | @uploaded_file.should_receive(:manipulate!).and_yield(img_frame) 161 | 162 | @uploaded_file.process! do |img| 163 | img.solarize 164 | end 165 | end 166 | 167 | it "should resize first and then yield to manipulate! if both a block and a size string are given" do 168 | img_frame = mock('an image frame') 169 | img_frame.should_receive(:solarize) 170 | 171 | @uploaded_file.should_receive(:resize!).with('200x200').ordered 172 | @uploaded_file.should_receive(:manipulate!).ordered.and_yield(img_frame) 173 | 174 | @uploaded_file.process!('200x200') do |img| 175 | img.solarize 176 | end 177 | end 178 | 179 | it "should do nothing if :none is passed" do 180 | @uploaded_file.should_not_receive(:manipulate!) 181 | @uploaded_file.process!(:none) 182 | end 183 | 184 | end 185 | 186 | 187 | -------------------------------------------------------------------------------- /spec/sanitized_file_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | require File.join(File.dirname(__FILE__), '../lib/upload_column/sanitized_file') 3 | begin 4 | require 'mime/types' 5 | rescue LoadError 6 | end 7 | 8 | describe "creating a new SanitizedFile" do 9 | it "should yield an empty file on empty String, nil, empty StringIO" do 10 | UploadColumn::SanitizedFile.new("").should be_empty 11 | UploadColumn::SanitizedFile.new(StringIO.new("")).should be_empty 12 | UploadColumn::SanitizedFile.new(nil).should be_empty 13 | file = mock('emptyfile') 14 | file.should_receive(:size).at_least(:once).and_return(0) 15 | UploadColumn::SanitizedFile.new(file).should be_empty 16 | end 17 | 18 | it "should yield a non empty file" do 19 | UploadColumn::SanitizedFile.new(stub_stringio('kerb.jpg', 'image/jpeg')).should_not be_empty 20 | UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', 'image/jpeg')).should_not be_empty 21 | end 22 | 23 | it "should not change a valid filename" do 24 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, "test.jpg")) 25 | t.filename.should == "test.jpg" 26 | end 27 | 28 | it "should remove illegal characters from a filename" do 29 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, "test-s,%&m#st?.jpg")) 30 | t.filename.should == "test-s___m_st_.jpg" 31 | end 32 | 33 | it "should remove slashes from the filename" do 34 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, "../../very_tricky/foo.bar")) 35 | t.filename.should_not =~ /[\\\/]/ 36 | end 37 | 38 | it "should remove illegal characters if there is no extension" do 39 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, '`*foo')) 40 | t.filename.should == "__foo" 41 | end 42 | 43 | it "should remove the path prefix on Windows" do 44 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, 'c:\temp\foo.txt')) 45 | t.filename.should == "foo.txt" 46 | end 47 | 48 | it "should make sure the *nix directory thingies can't be used as filenames" do 49 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, ".")) 50 | t.filename.should == "_." 51 | end 52 | 53 | it "should downcase uppercase filenames" do 54 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', nil, "DSC4056.JPG")) 55 | t.filename.should == "dsc4056.jpg" 56 | end 57 | 58 | end 59 | 60 | # Note that SanitizedFile#path and #exists? need to be checked seperately as the return values will vary 61 | describe "all sanitized files", :shared => true do 62 | 63 | it "should not be empty" do 64 | @file.should_not be_empty 65 | end 66 | 67 | it "should return the original filename" do 68 | @file.original_filename.should == "kerb.jpg" 69 | end 70 | 71 | it "should return the filename" do 72 | @file.filename.should == "kerb.jpg" 73 | end 74 | 75 | it "should return the basename" do 76 | @file.basename.should == "kerb" 77 | end 78 | 79 | it "should return the extension" do 80 | @file.extension.should == "jpg" 81 | end 82 | 83 | it "should be moved to the correct location" do 84 | @file.move_to(public_path('gurr.jpg')) 85 | File.exists?( public_path('gurr.jpg') ).should === true 86 | file_path('kerb.jpg').should be_identical_with(public_path('gurr.jpg')) 87 | end 88 | 89 | it "should have changed its path when moved" do 90 | @file.move_to(public_path('gurr.jpg')) 91 | @file.path.should match_path(public_path('gurr.jpg')) 92 | end 93 | 94 | it "should have changed its filename when moved" do 95 | @file.filename # Make sure the filename has been cached 96 | @file.move_to(public_path('gurr.jpg')) 97 | @file.filename.should == 'gurr.jpg' 98 | end 99 | 100 | it "should have split the filename when moved" do 101 | @file.move_to(public_path('gurr.monk')) 102 | @file.basename.should == 'gurr' 103 | @file.extension.should == 'monk' 104 | end 105 | 106 | it "should be copied to the correct location" do 107 | @file.copy_to(public_path('gurr.jpg')) 108 | File.exists?( public_path('gurr.jpg') ).should === true 109 | file_path('kerb.jpg').should be_identical_with(public_path('gurr.jpg')) 110 | end 111 | 112 | it "should not have changed its path when copied" do 113 | running { @file.copy_to(public_path('gurr.jpg')) }.should_not change(@file, :path) 114 | end 115 | 116 | it "should not have changed its filename when copied" do 117 | running { @file.copy_to(public_path('gurr.jpg')) }.should_not change(@file, :filename) 118 | end 119 | 120 | it "should return an object of the same class when copied" do 121 | new_file = @file.copy_to(public_path('gurr.jpg')) 122 | new_file.should be_an_instance_of(@file.class) 123 | end 124 | 125 | it "should adjust the path of the object that is returned when copied" do 126 | new_file = @file.copy_to(public_path('gurr.jpg')) 127 | new_file.path.should match_path(public_path('gurr.jpg')) 128 | end 129 | 130 | it "should adjust the filename of the object that is returned when copied" do 131 | @file.filename # Make sure the filename has been cached 132 | @file = @file.copy_to(public_path('gurr.monk')) 133 | @file.filename.should == 'gurr.monk' 134 | end 135 | 136 | it "should split the filename of the object that is returned when copied" do 137 | @file = @file.copy_to(public_path('gurr.monk')) 138 | @file.basename.should == 'gurr' 139 | @file.extension.should == 'monk' 140 | end 141 | 142 | after do 143 | FileUtils.rm_rf(PUBLIC) 144 | end 145 | end 146 | 147 | describe "a sanitized Tempfile" do 148 | before do 149 | @tempfile = stub_tempfile('kerb.jpg', 'image/jpeg') 150 | @file = UploadColumn::SanitizedFile.new(@tempfile) 151 | end 152 | 153 | it_should_behave_like "all sanitized files" 154 | 155 | it "should not raise an error when moved to its own location" do 156 | running { @file.move_to(@file.path) }.should_not raise_error 157 | end 158 | 159 | it "should return a new instance when copied to its own location" do 160 | running { 161 | new_file = @file.copy_to(@file.path) 162 | new_file.should be_an_instance_of(@file.class) 163 | }.should_not raise_error 164 | end 165 | 166 | it "should exist" do 167 | @file.should be_in_existence 168 | end 169 | 170 | it "should return the correct path" do 171 | @file.path.should_not == nil 172 | @file.path.should == @tempfile.path 173 | end 174 | end 175 | 176 | describe "a sanitized StringIO" do 177 | before do 178 | @file = UploadColumn::SanitizedFile.new(stub_stringio('kerb.jpg', 'image/jpeg')) 179 | end 180 | 181 | it_should_behave_like "all sanitized files" 182 | 183 | it "should not exist" do 184 | @file.should_not be_in_existence 185 | end 186 | 187 | it "should return no path" do 188 | @file.path.should == nil 189 | end 190 | 191 | end 192 | 193 | describe "a sanitized File object" do 194 | before do 195 | @file = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', 'image/jpeg')) 196 | @file.should_not be_empty 197 | end 198 | 199 | it_should_behave_like "all sanitized files" 200 | 201 | it "should not raise an error when moved to its own location" do 202 | running { @file.move_to(@file.path) }.should_not raise_error 203 | end 204 | 205 | it "should return a new instance when copied to its own location" do 206 | running { 207 | new_file = @file.copy_to(@file.path) 208 | new_file.should be_an_instance_of(@file.class) 209 | }.should_not raise_error 210 | end 211 | 212 | it "should exits" do 213 | @file.should be_in_existence 214 | end 215 | 216 | it "should return correct path" do 217 | @file.path.should match_path(file_path('kerb.jpg')) 218 | end 219 | end 220 | 221 | describe "a SanitizedFile opened from a path" do 222 | before do 223 | @file = UploadColumn::SanitizedFile.new(file_path('kerb.jpg')) 224 | @file.should_not be_empty 225 | end 226 | 227 | it_should_behave_like "all sanitized files" 228 | 229 | it "should not raise an error when moved to its own location" do 230 | running { @file.move_to(@file.path) }.should_not raise_error 231 | end 232 | 233 | it "should return a new instance when copied to its own location" do 234 | running { 235 | new_file = @file.copy_to(@file.path) 236 | new_file.should be_an_instance_of(@file.class) 237 | }.should_not raise_error 238 | end 239 | 240 | it "should exits" do 241 | @file.should be_in_existence 242 | end 243 | 244 | it "should return correct path" do 245 | @file.path.should == file_path('kerb.jpg') 246 | end 247 | end 248 | 249 | describe "an empty SanitizedFile" do 250 | before do 251 | @empty = UploadColumn::SanitizedFile.new(nil) 252 | end 253 | 254 | it "should be empty" do 255 | @empty.should be_empty 256 | end 257 | 258 | it "should not exist" do 259 | @empty.should_not be_in_existence 260 | end 261 | 262 | it "should have no size" do 263 | @empty.size.should == nil 264 | end 265 | 266 | it "should have no path" do 267 | @empty.path.should == nil 268 | end 269 | 270 | it "should have no original filename" do 271 | @empty.original_filename.should == nil 272 | end 273 | 274 | it "should have no filename" do 275 | @empty.filename.should == nil 276 | end 277 | 278 | it "should have no basename" do 279 | @empty.basename.should == nil 280 | end 281 | 282 | it "should have no extension" do 283 | @empty.extension.should == nil 284 | end 285 | end 286 | 287 | describe "a SanitizedFile" do 288 | 289 | before do 290 | @file = UploadColumn::SanitizedFile.new(stub_tempfile('kerb.jpg', 'image/jpeg')) 291 | end 292 | 293 | it "should properly split into basename and extension" do 294 | @file.basename.should == "kerb" 295 | @file.extension.should == "jpg" 296 | end 297 | 298 | it "should do a system call" do 299 | @file.send(:system_call, 'echo "monkey"').chomp.should == "monkey" 300 | end 301 | 302 | end 303 | 304 | describe "a SanizedFile with a complex filename" do 305 | it "properly split into basename and extension" do 306 | t = UploadColumn::SanitizedFile.new(stub_tempfile('kerb.jpg', nil, 'complex.filename.tar.gz')) 307 | t.basename.should == "complex.filename" 308 | t.extension.should == "tar.gz" 309 | end 310 | end 311 | 312 | # FIXME: figure out why this doesn't run 313 | #describe "determinating the mime-type with a *nix exec" do 314 | # 315 | # before do 316 | # @file = stub_file('kerb.jpg', nil, 'harg.css') 317 | # @sanitized = UploadColumn::SanitizedFile.new(@file) 318 | # end 319 | # 320 | # it "should chomp and return if it has no encoding" do 321 | # @sanitized.should_receive(:system_call).with(%(file -bi "#{@file.path}")).and_return("image/jpeg\n") 322 | # 323 | # @sanitized.send(:get_content_type_from_exec) #.should == "image/jpeg" 324 | # end 325 | # 326 | # it "should chomp and return and chop off the encoding if it has one" do 327 | # @sanitized.should_receive(:system_call).with(%(file -bi "#{@file.path}")).and_return("text/plain; charset=utf-8;\n") 328 | # 329 | # @sanitized.send(:get_content_type_from_exec) #.should == "text/plain" 330 | # end 331 | # 332 | # it "should not crap out when something weird happens" do 333 | # @sanitized.should_receive(:system_call).with(%(file -bi "#{@file.path}")).and_return("^blah//(?)wtf???") 334 | # 335 | # @sanitized.send(:get_content_type_from_exec).should == nil 336 | # end 337 | # 338 | #end 339 | 340 | describe "The mime-type of a Sanitized File" do 341 | 342 | before do 343 | @file = stub_file('kerb.jpg', nil, 'harg.css') 344 | end 345 | 346 | # TODO: refactor this test so it mocks out system_call 347 | it "should be determined via *nix exec" do 348 | 349 | @sanitized = UploadColumn::SanitizedFile.new(@file, :get_content_type_from_file_exec => true) 350 | 351 | @sanitized.stub!(:path).and_return('/path/to/file.jpg') 352 | @sanitized.should_receive(:system_call).with(%(file -bi "/path/to/file.jpg")).and_return('text/monkey') 353 | 354 | @sanitized.content_type.should == "text/monkey" 355 | end 356 | 357 | it "shouldn't choke up when the *nix exec errors out" do 358 | @sanitized = UploadColumn::SanitizedFile.new(@file, :get_content_type_from_file_exec => true) 359 | 360 | lambda { 361 | @sanitized.should_receive(:system_call).and_raise('monkey') 362 | @sanitized.content_type 363 | }.should_not raise_error 364 | end 365 | 366 | it "should otherwise be loaded from MIME::Types" do 367 | if defined?(MIME::Types) 368 | @sanitized = UploadColumn::SanitizedFile.new(@file) 369 | 370 | @sanitized.should_receive(:get_content_type_from_exec).and_return(nil) # Make sure the *nix exec isn't interfering 371 | @sanitized.content_type.should == "text/css" 372 | else 373 | puts "WARNING: Could not run all examples because MIME::Types is not defined, try installing the mime-types gem!" 374 | end 375 | end 376 | 377 | it "should be taken from the browser if all else fails" do 378 | @sanitized = UploadColumn::SanitizedFile.new(@file) 379 | 380 | @file.should_receive(:content_type).at_least(:once).and_return('application/xhtml+xml') # Set up browser behavior 381 | # FIXME: this is brittle. There really should be another way of changing this behaviour. 382 | @sanitized.should_receive(:get_content_type_from_mime_types).and_return(nil) # Make sure MIME::Types isn't interfering 383 | @sanitized.content_type.should == "application/xhtml+xml" 384 | end 385 | end 386 | 387 | describe "a SanitizedFile with a wrong extension" do 388 | 389 | # This test currently always fails if MIME::Types is unavailable, 390 | # TODO: come up with a clever way to stub out the content_type-y behaviour. 391 | it "should fix extention if fix_file_extensions is true" do 392 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', 'image/jpeg', 'kerb.css'), :fix_file_extensions => true) 393 | 394 | t.content_type.should == "image/jpeg" 395 | t.extension.should == "jpeg" 396 | t.filename.should == "kerb.jpeg" 397 | end 398 | 399 | it "should not fix extention if fix_file_extensions is false" do 400 | t = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', 'image/jpeg', 'kerb.css'), :fix_file_extensions => false) 401 | 402 | #t.content_type.should == "image/css" FIXME: the result of this is upredictable and 403 | # differs, depending on whether or not the user has MIME::Types installed 404 | t.extension.should == "css" 405 | t.filename.should == "kerb.css" 406 | end 407 | end 408 | 409 | describe "copying a sanitized Tempfile with permissions set" do 410 | before do 411 | @file = UploadColumn::SanitizedFile.new(stub_tempfile('kerb.jpg', 'image/jpeg'), :permissions => 0755) 412 | @file = @file.copy_to(public_path('gurr.jpg')) 413 | end 414 | 415 | it "should set the right permissions" do 416 | @file.should have_permissions(0755) 417 | end 418 | end 419 | 420 | describe "copying a sanitized StringIO with permissions set" do 421 | before do 422 | @file = UploadColumn::SanitizedFile.new(stub_stringio('kerb.jpg', 'image/jpeg'), :permissions => 0755) 423 | @file = @file.copy_to(public_path('gurr.jpg')) 424 | end 425 | 426 | it "should set the right permissions" do 427 | @file.should have_permissions(0755) 428 | end 429 | end 430 | 431 | describe "copying a sanitized File object with permissions set" do 432 | before do 433 | @file = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', 'image/jpeg'), :permissions => 0755) 434 | @file = @file.copy_to(public_path('gurr.jpg')) 435 | end 436 | 437 | it "should set the right permissions" do 438 | @file.should have_permissions(0755) 439 | end 440 | end 441 | 442 | describe "copying a sanitized file by path with permissions set" do 443 | before do 444 | @file = UploadColumn::SanitizedFile.new(file_path('kerb.jpg'), :permissions => 0755) 445 | @file = @file.copy_to(public_path('gurr.jpg')) 446 | end 447 | 448 | it "should set the right permissions" do 449 | @file.should have_permissions(0755) 450 | end 451 | end 452 | 453 | 454 | describe "moving a sanitized Tempfile with permissions set" do 455 | before do 456 | @file = UploadColumn::SanitizedFile.new(stub_tempfile('kerb.jpg', 'image/jpeg'), :permissions => 0755) 457 | @file.move_to(public_path('gurr.jpg')) 458 | end 459 | 460 | it "should set the right permissions" do 461 | @file.should have_permissions(0755) 462 | end 463 | end 464 | 465 | describe "moving a sanitized StringIO with permissions set" do 466 | before do 467 | @file = UploadColumn::SanitizedFile.new(stub_stringio('kerb.jpg', 'image/jpeg'), :permissions => 0755) 468 | @file.move_to(public_path('gurr.jpg')) 469 | end 470 | 471 | it "should set the right permissions" do 472 | @file.should have_permissions(0755) 473 | end 474 | end 475 | 476 | describe "moving a sanitized File object with permissions set" do 477 | before do 478 | @file = UploadColumn::SanitizedFile.new(stub_file('kerb.jpg', 'image/jpeg'), :permissions => 0755) 479 | @file.move_to(public_path('gurr.jpg')) 480 | end 481 | 482 | it "should set the right permissions" do 483 | @file.should have_permissions(0755) 484 | end 485 | end 486 | 487 | describe "moving a sanitized file by path with permissions set" do 488 | before do 489 | @file = UploadColumn::SanitizedFile.new(file_path('kerb.jpg'), :permissions => 0755) 490 | @file.move_to(public_path('gurr.jpg')) 491 | end 492 | 493 | it "should set the right permissions" do 494 | @file.should have_permissions(0755) 495 | end 496 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'rubygems' 3 | require 'spec' 4 | require 'ruby-debug' 5 | 6 | require File.join(File.dirname(__FILE__), 'custom_matchers') 7 | 8 | RAILS_ROOT = File.expand_path(File.dirname(__FILE__)) unless defined?(RAILS_ROOT) 9 | PUBLIC = File.expand_path(File.join(RAILS_ROOT, 'public')) unless defined?(PUBLIC) 10 | 11 | def file_path( filename ) 12 | File.join(File.dirname(__FILE__), 'fixtures', filename) 13 | end 14 | 15 | def public_path( filename ) 16 | File.join(File.dirname(__FILE__), 'public', filename) 17 | end 18 | 19 | def stub_tempfile(filename, mime_type=nil, fake_name=nil) 20 | raise "#{path} file does not exist" unless File.exist?(file_path(filename)) 21 | 22 | t = Tempfile.new(filename) 23 | FileUtils.copy_file(file_path(filename), t.path) 24 | 25 | t.stub!(:original_filename).and_return(fake_name || filename) 26 | t.stub!(:content_type).and_return(mime_type) 27 | t.stub!(:local_path).and_return(t.path) 28 | return t 29 | end 30 | 31 | def stub_stringio(filename, mime_type=nil, fake_name=nil) 32 | if filename 33 | t = StringIO.new( IO.read( file_path( filename ) ) ) 34 | else 35 | t = StringIO.new 36 | end 37 | t.stub!(:local_path).and_return("") 38 | t.stub!(:original_filename).and_return(filename || fake_name) 39 | t.stub!(:content_type).and_return(mime_type) 40 | return t 41 | end 42 | 43 | def stub_file(filename, mime_type=nil, fake_name=nil) 44 | f = File.open(file_path(filename)) 45 | f.stub!(:content_type).and_return(mime_type) 46 | f.stub!(:original_filename).and_return(fake_name) if fake_name 47 | return f 48 | end 49 | 50 | module UploadColumnSpecHelper 51 | 52 | def disconnected_model(model_class) 53 | model_class.stub!(:columns).and_return([]) 54 | return model_class.new 55 | end 56 | 57 | def setup_standard_mocking 58 | @options = mock('options', :null_object => true) 59 | Entry.upload_column :avatar, @options 60 | @entry = disconnected_model(Entry) 61 | mock_file 62 | end 63 | 64 | def setup_version_mocking 65 | Entry.upload_column :avatar, :versions => [ :thumb, :large ] 66 | @entry = disconnected_model(Entry) 67 | mock_file 68 | end 69 | 70 | private 71 | 72 | def mock_file 73 | @file = mock('file') 74 | 75 | @uploaded_file = mock('uploaded_file') 76 | @uploaded_file.stub!(:actual_filename).and_return('monkey.png') 77 | end 78 | end 79 | 80 | module UniversalSpecHelper 81 | 82 | def running(&block) 83 | lambda(&block) 84 | end 85 | 86 | end 87 | 88 | Spec::Runner.configure do |config| 89 | config.include UniversalSpecHelper 90 | end -------------------------------------------------------------------------------- /spec/upload_column_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | gem 'activerecord' 4 | require 'active_record' 5 | 6 | require File.join(File.dirname(__FILE__), '../lib/upload_column') 7 | 8 | describe "UploadColumn" do 9 | 10 | it "should have a default configuration" do 11 | UploadColumn.configuration.should be_an_instance_of(Hash) 12 | config = UploadColumn.configuration 13 | 14 | config[:tmp_dir].should == 'tmp' 15 | config[:store_dir].should be_an_instance_of(Proc) 16 | config[:root_dir].should == File.join(RAILS_ROOT, 'public') 17 | config[:get_content_type_from_file_exec].should == true 18 | config[:fix_file_extensions].should == false 19 | config[:process].should == nil 20 | config[:permissions].should == 0644 21 | config[:extensions].should == UploadColumn.extensions 22 | config[:web_root].should == '' 23 | config[:manipulator].should == nil 24 | config[:versions].should == nil 25 | config[:validate_integrity].should == false 26 | end 27 | 28 | it "should have a list of allowed extensions" do 29 | UploadColumn.extensions.should == %w(asf ai avi doc dvi dwg eps gif gz jpg jpeg mov mp3 mpeg odf pac pdf png ppt psd swf swx tar tar.gz torrent txt wmv wav xls zip) 30 | end 31 | 32 | it "should have a list of allowed image extensions" do 33 | UploadColumn.image_extensions.should == %w(jpg jpeg gif png) 34 | end 35 | 36 | end 37 | 38 | describe "UploadColumn.configure" do 39 | 40 | after do 41 | UploadColumn.reset_configuration 42 | end 43 | 44 | it "should yield a configuration proxy" do 45 | UploadColumn.configure do |config| 46 | config.should be_an_instance_of(UploadColumn::ConfigurationProxy) 47 | end 48 | end 49 | 50 | it "should change the configuration of a known option" do 51 | UploadColumn.configure do |config| 52 | config.web_root = "/monkey" 53 | end 54 | 55 | UploadColumn.configuration[:web_root].should == "/monkey" 56 | end 57 | 58 | it "should change the configuration of an unknown option" do 59 | UploadColumn.configure do |config| 60 | config.monkey = ":)" 61 | end 62 | 63 | UploadColumn.configuration[:monkey].should == ":)" 64 | end 65 | end -------------------------------------------------------------------------------- /spec/uploaded_file_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | require 'active_record' 4 | 5 | require File.join(File.dirname(__FILE__), '../lib/upload_column') 6 | 7 | ActiveRecord::Base.send(:include, UploadColumn) 8 | 9 | describe "uploading a file" do 10 | it "should trigger an _after_upload callback" do 11 | record = mock('a record') 12 | record.should_receive(:avatar_after_upload) 13 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), record, :avatar) 14 | end 15 | end 16 | 17 | describe "all uploaded files", :shared => true do 18 | it "should not be empty" do 19 | @file.should_not be_empty 20 | end 21 | 22 | it "should return the correct filesize" do 23 | @file.size.should == 87582 24 | end 25 | 26 | it "should return the original filename" do 27 | @file.original_filename.should == "kerb.jpg" 28 | end 29 | 30 | it "should return the filename" do 31 | @file.filename.should == "kerb.jpg" 32 | end 33 | 34 | it "should return the basename" do 35 | @file.basename.should == "kerb" 36 | end 37 | 38 | it "should return the extension" do 39 | @file.extension.should == "jpg" 40 | end 41 | 42 | after do 43 | FileUtils.rm_rf(public_path('*')) 44 | end 45 | end 46 | 47 | describe "an uploaded tempfile" do 48 | 49 | before do 50 | @file = UploadColumn::UploadedFile.upload(stub_tempfile('kerb.jpg')) 51 | end 52 | 53 | it_should_behave_like "all uploaded files" 54 | 55 | it "should return the correct path" do 56 | @file.path.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}, 'kerb.jpg') 57 | end 58 | 59 | it "should return the correct relative_path" do 60 | @file.relative_path.should =~ %r{^tmp/((\d+\.)+\d+)/kerb.jpg} 61 | end 62 | 63 | it "should return correct dir" do 64 | @file.dir.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}) 65 | end 66 | end 67 | 68 | describe "an uploaded StringIO" do 69 | 70 | before do 71 | @file = UploadColumn::UploadedFile.upload(stub_stringio('kerb.jpg')) 72 | end 73 | 74 | it_should_behave_like "all uploaded files" 75 | 76 | it "should return the correct path" do 77 | @file.path.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}, 'kerb.jpg') 78 | end 79 | 80 | it "should return the correct relative_path" do 81 | @file.relative_path.should =~ %r{^tmp/((\d+\.)+\d+)/kerb.jpg} 82 | end 83 | 84 | it "should return correct dir" do 85 | @file.dir.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}) 86 | end 87 | end 88 | 89 | describe "an uploaded File object" do 90 | 91 | before do 92 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg')) 93 | end 94 | 95 | it_should_behave_like "all uploaded files" 96 | 97 | it "should return the correct path" do 98 | @file.path.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}, 'kerb.jpg') 99 | end 100 | 101 | it "should return the correct relative_path" do 102 | @file.relative_path.should =~ %r{^tmp/((\d+\.)+\d+)/kerb.jpg} 103 | end 104 | 105 | it "should return correct dir" do 106 | @file.dir.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}) 107 | end 108 | end 109 | 110 | describe "an uploaded non-empty String" do 111 | it "should raise an error" do 112 | lambda do 113 | UploadColumn::UploadedFile.upload("../README") 114 | end.should raise_error(UploadColumn::UploadNotMultipartError) 115 | end 116 | end 117 | 118 | describe "an uploded empty file" do 119 | it "should return nil" do 120 | file = mock('uploaded empty file') 121 | file.should_receive(:empty?).and_return(true) 122 | upload = mock('upload') 123 | UploadColumn::UploadedFile.should_receive(:new).and_return(file) 124 | 125 | UploadColumn::UploadedFile.upload(upload).should == nil 126 | end 127 | end 128 | 129 | describe "an UploadedFile" do 130 | before do 131 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, :donkey) 132 | end 133 | 134 | it "should have the correct relative store dir" do 135 | @file.relative_store_dir.should == 'donkey' 136 | end 137 | 138 | it "should have the correct store dir" do 139 | @file.store_dir.should == File.expand_path('donkey', PUBLIC) 140 | end 141 | 142 | it "should have the correct relative tmp dir" do 143 | @file.relative_tmp_dir.should == 'tmp' 144 | end 145 | 146 | it "should have the correct tmp dir" do 147 | @file.tmp_dir.should == File.expand_path('tmp', PUBLIC) 148 | end 149 | 150 | it "should return something sensible on inspect" do 151 | @file.inspect.should == "" 152 | end 153 | end 154 | 155 | describe "an UploadedFile where store_dir is a String" do 156 | before do 157 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, nil, :store_dir => 'monkey') 158 | end 159 | 160 | it "should have the correct relative store dir" do 161 | @file.relative_store_dir.should == 'monkey' 162 | end 163 | 164 | it "should have the correct store dir" do 165 | @file.store_dir.should == File.expand_path('monkey', PUBLIC) 166 | end 167 | end 168 | 169 | describe "an UploadedFile where tmp_dir is a String" do 170 | before do 171 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, nil, :tmp_dir => 'monkey') 172 | end 173 | 174 | it "should have the correct relative tmp dir" do 175 | @file.relative_tmp_dir.should == 'monkey' 176 | end 177 | 178 | it "should have the correct tmp dir" do 179 | @file.tmp_dir.should == File.expand_path('monkey', PUBLIC) 180 | end 181 | end 182 | 183 | describe "an UploadedFile where filename is a String" do 184 | before do 185 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, nil, :filename => 'monkey.png', :versions => [:thumb, :large]) 186 | end 187 | 188 | it "should have the correct filename" do 189 | @file.filename.should == 'monkey.png' 190 | end 191 | 192 | it "should remember the actual filename" do 193 | @file.actual_filename.should == "kerb.jpg" 194 | end 195 | 196 | it "should have versions with the correct filename" do 197 | @file.thumb.filename.should == 'monkey.png' 198 | @file.large.filename.should == 'monkey.png' 199 | end 200 | end 201 | 202 | describe "an UploadedFile where filename is a Proc with the record piped in" do 203 | before do 204 | record = mock('a record') 205 | record.stub!(:name).and_return('quack') 206 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), record, nil, :versions => [:thumb, :large], :filename => proc{ |r| r.name }) 207 | end 208 | 209 | it "should have the correct filename" do 210 | @file.filename.should == 'quack' 211 | end 212 | 213 | it "should remember the actual filename" do 214 | @file.actual_filename.should == "kerb.jpg" 215 | end 216 | 217 | it "should have versions with the correct filename" do 218 | @file.thumb.filename.should == 'quack' 219 | @file.large.filename.should == 'quack' 220 | end 221 | end 222 | 223 | describe "an UploadedFile where filename is a Proc with the record and file piped in" do 224 | before do 225 | record = mock('a record') 226 | record.stub!(:name).and_return('quack') 227 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), record, nil, :versions => [:thumb, :large], :filename => proc{ |r, f| "#{r.name}-#{f.basename}-#{f.suffix}quox.#{f.extension}"}) 228 | end 229 | 230 | it "should have the correct filename" do 231 | @file.filename.should == 'quack-kerb-quox.jpg' 232 | end 233 | 234 | it "should remember the actual filename" do 235 | @file.actual_filename.should == "kerb.jpg" 236 | end 237 | 238 | it "should have versions with the correct filename" do 239 | @file.thumb.filename.should == 'quack-kerb-thumbquox.jpg' 240 | @file.large.filename.should == 'quack-kerb-largequox.jpg' 241 | end 242 | end 243 | 244 | describe "an UploadedFile with a filename callback" do 245 | before do 246 | @instance = mock('instance with filename callback') 247 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), @instance, :monkey, :versions => [:thumb, :large]) 248 | end 249 | 250 | it "should have the correct filename" do 251 | @instance.should_receive(:monkey_filename).with(@file).and_return("llama") 252 | @file.filename.should == 'llama' 253 | end 254 | 255 | it "should remember the actual filename" do 256 | @file.actual_filename.should == "kerb.jpg" 257 | end 258 | 259 | it "should have versions with the correct filename" do 260 | @instance.should_receive(:monkey_filename).with(@file.thumb).and_return("barr") 261 | @instance.should_receive(:monkey_filename).with(@file.large).and_return("quox") 262 | @file.thumb.filename.should == 'barr' 263 | @file.large.filename.should == 'quox' 264 | end 265 | end 266 | 267 | describe "uploading an UploadedFile where filename is a Proc" do 268 | before do 269 | record = mock('a record') 270 | record.stub!(:name).and_return('quack') 271 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), record, nil, :versions => [:thumb, :large], :filename => proc{ |r, f| "#{r.name}-#{f.basename}-#{f.suffix}quox.#{f.extension}"}) 272 | end 273 | 274 | it "should have the correct filename" do 275 | @file.filename.should == 'quack-kerb-quox.jpg' 276 | end 277 | 278 | it "should remember the actual filename" do 279 | @file.actual_filename.should == "kerb.jpg" 280 | end 281 | 282 | it "should have versions with the correct filename" do 283 | @file.thumb.filename.should == 'quack-kerb-thumbquox.jpg' 284 | @file.large.filename.should == 'quack-kerb-largequox.jpg' 285 | end 286 | 287 | it "should have a correct path" do 288 | @file.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'quack-kerb-quox.jpg' ) 289 | end 290 | 291 | it "should have versions with correct paths" do 292 | @file.thumb.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'quack-kerb-thumbquox.jpg' ) 293 | @file.large.path.should match_path(PUBLIC, 'tmp', /(?:\d+\.)+\d+/, 'quack-kerb-largequox.jpg' ) 294 | end 295 | end 296 | 297 | describe "an UploadedFile where store_dir is a simple Proc" do 298 | before do 299 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, nil, :store_dir => proc{'monkey'}) 300 | end 301 | 302 | it "should have the correct relative store dir" do 303 | @file.relative_store_dir.should == 'monkey' 304 | end 305 | 306 | it "should have the correct store dir" do 307 | @file.store_dir.should == File.expand_path('monkey', PUBLIC) 308 | end 309 | end 310 | 311 | describe "an UploadedFile where tmp_dir is a simple Proc" do 312 | before do 313 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, nil, :tmp_dir => proc{'monkey'}) 314 | end 315 | 316 | it "should have the correct relative tmp dir" do 317 | @file.relative_tmp_dir.should == 'monkey' 318 | end 319 | 320 | it "should have the correct tmp dir" do 321 | @file.tmp_dir.should == File.expand_path('monkey', PUBLIC) 322 | end 323 | end 324 | 325 | describe "an UploadedFile where store_dir is a Proc and has the record piped in" do 326 | before do 327 | record = mock('a record') 328 | record.stub!(:name).and_return('quack') 329 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), record, nil, :store_dir => proc{ |record| File.join(record.name, 'monkey')}) 330 | end 331 | 332 | it "should have the correct relative store dir" do 333 | @file.relative_store_dir.should == 'quack/monkey' 334 | end 335 | 336 | it "should have the correct store dir" do 337 | @file.store_dir.should == File.expand_path('quack/monkey', PUBLIC) 338 | end 339 | end 340 | 341 | describe "an UploadedFile where tmp_dir is a Proc and has the record piped in" do 342 | before do 343 | record = mock('a record') 344 | record.stub!(:name).and_return('quack') 345 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), record, nil, :tmp_dir => proc{ |record| File.join(record.name, 'monkey')}) 346 | end 347 | 348 | it "should have the correct relative tmp dir" do 349 | @file.relative_tmp_dir.should == 'quack/monkey' 350 | end 351 | 352 | it "should have the correct tmp dir" do 353 | @file.tmp_dir.should == File.expand_path('quack/monkey', PUBLIC) 354 | end 355 | end 356 | 357 | 358 | describe "an UploadedFile where store_dir is a Proc and has the record and file piped in" do 359 | before do 360 | record = mock('a record') 361 | record.stub!(:name).and_return('quack') 362 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), record, nil, :store_dir => proc{ |r, f| File.join(record.name, f.basename, 'monkey')}) 363 | end 364 | 365 | it "should have the correct relative store dir" do 366 | @file.relative_store_dir.should == 'quack/kerb/monkey' 367 | end 368 | 369 | it "should have the correct store dir" do 370 | @file.store_dir.should == File.expand_path('quack/kerb/monkey', PUBLIC) 371 | end 372 | end 373 | 374 | describe "an UploadedFile where tmp_dir is a Proc and has the record and file piped in" do 375 | before do 376 | record = mock('a record') 377 | record.stub!(:name).and_return('quack') 378 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), record, nil, :tmp_dir => proc{ |r, f| File.join(record.name, f.basename, 'monkey')}) 379 | end 380 | 381 | it "should have the correct relative tmp dir" do 382 | @file.relative_tmp_dir.should == 'quack/kerb/monkey' 383 | end 384 | 385 | it "should have the correct tmp dir" do 386 | @file.tmp_dir.should == File.expand_path('quack/kerb/monkey', PUBLIC) 387 | end 388 | end 389 | 390 | 391 | describe "an UploadedFile with a store_dir callback" do 392 | before do 393 | i = mock('instance with store_dir callback') 394 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), i, :monkey) 395 | i.should_receive(:monkey_store_dir).with(@file).and_return('llama') 396 | end 397 | 398 | it "should have the correct relative store dir" do 399 | @file.relative_store_dir.should == 'llama' 400 | end 401 | 402 | it "should have the correct store dir" do 403 | @file.store_dir.should == File.expand_path('llama', PUBLIC) 404 | end 405 | end 406 | 407 | describe "an UploadedFile with a tmp_dir callback" do 408 | before do 409 | i = mock('instance with a tmp_dir callback') 410 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), i, :monkey) 411 | i.should_receive(:monkey_tmp_dir).with(@file).and_return('gorilla') 412 | end 413 | 414 | it "should have the correct relative tmp dir" do 415 | @file.relative_tmp_dir.should == 'gorilla' 416 | end 417 | 418 | it "should have the correct tmp dir" do 419 | @file.tmp_dir.should == File.expand_path('gorilla', PUBLIC) 420 | end 421 | end 422 | 423 | describe "an UploadedFile that has just been uploaded" do 424 | 425 | before do 426 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey) 427 | end 428 | 429 | it_should_behave_like "all uploaded files" 430 | 431 | it "should be new" do 432 | @file.should be_new_file 433 | end 434 | 435 | it "should be a tempfile" do 436 | @file.should be_a_tempfile 437 | end 438 | 439 | it "should exist" do 440 | @file.should be_in_existence 441 | end 442 | 443 | it "should be stored in tmp" do 444 | @file.path.should match_path('public', 'tmp', %r{((\d+\.)+\d+)}, 'kerb.jpg') 445 | end 446 | 447 | end 448 | 449 | describe "saving an UploadedFile" do 450 | before do 451 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey) 452 | end 453 | 454 | it "should return true" do 455 | @file.send(:save).should === true 456 | end 457 | 458 | it "should copy the file to the correct location" do 459 | @file.send(:save) 460 | @file.path.should match_path('public', 'monkey', 'kerb.jpg') 461 | @file.should be_in_existence 462 | end 463 | 464 | after do 465 | FileUtils.rm_rf(PUBLIC) 466 | end 467 | 468 | end 469 | 470 | describe "a saved UploadedFile" do 471 | before do 472 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey) 473 | @file.send(:save) 474 | end 475 | 476 | it_should_behave_like "all uploaded files" 477 | 478 | it "should not be new" do 479 | @file.should_not be_new_file 480 | end 481 | 482 | it "should not be a tempfile" do 483 | @file.should_not be_a_tempfile 484 | end 485 | 486 | it "should return the correct path" do 487 | @file.path.should match_path('public', 'monkey', 'kerb.jpg') 488 | end 489 | 490 | it "should return the correct relative_path" do 491 | @file.relative_path.should == "monkey/kerb.jpg" 492 | end 493 | 494 | it "should return the correct dir" do 495 | @file.dir.should match_path('public', 'monkey') 496 | end 497 | 498 | after do 499 | FileUtils.rm_rf(PUBLIC) 500 | end 501 | 502 | end 503 | 504 | describe "an UploadedFile with a manipulator" do 505 | before do 506 | a_manipulator = Module.new 507 | a_manipulator.send(:define_method, :monkey! ) { |stuff| stuff } 508 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, :donkey, :manipulator => a_manipulator) 509 | end 510 | 511 | it "should extend the object with the manipulator methods." do 512 | @file.should respond_to(:monkey!) 513 | end 514 | 515 | end 516 | 517 | describe "an UploadedFile with a manipulator and versions" do 518 | before do 519 | a_manipulator = Module.new 520 | a_manipulator.send(:define_method, :monkey! ) { |stuff| stuff } 521 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, :donkey, :versions => [ :thumb, :large ], :manipulator => a_manipulator) 522 | end 523 | 524 | it "should extend the object with the manipulator methods." do 525 | @file.should respond_to(:monkey!) 526 | end 527 | 528 | it "should extend the versions with the manipulator methods." do 529 | @file.thumb.should respond_to(:monkey!) 530 | @file.large.should respond_to(:monkey!) 531 | end 532 | 533 | end 534 | 535 | describe "an UploadedFile with a manipulator with dependencies" do 536 | 537 | it "should extend the object with the manipulator methods and load dependencies." do 538 | process_proxy = mock('nothing in particular') 539 | a_manipulator = Module.new 540 | a_manipulator.send(:define_method, :monkey! ) { |stuff| stuff } 541 | a_manipulator.send(:define_method, :load_manipulator_dependencies) do 542 | # horrible abuse of Ruby's closures. This allows us to set expectations on the process_proxy 543 | # and if process! is called, the process_proxy will be adressed instead. 544 | process_proxy.load_manipulator_dependencies 545 | end 546 | 547 | process_proxy.should_receive(:load_manipulator_dependencies) 548 | 549 | @file = UploadColumn::UploadedFile.new(:open, stub_file('kerb.jpg'), nil, :donkey, :manipulator => a_manipulator) 550 | 551 | @file.should respond_to(:monkey!) 552 | end 553 | 554 | end 555 | 556 | describe "an UploadedFile with a manipulator and process instruction" do 557 | 558 | it "should process before iterating versions" do 559 | process_proxy = mock('nothing in particular') 560 | a_manipulator = Module.new 561 | a_manipulator.send(:define_method, :process!) do |*args| 562 | process_proxy.process!(*args) 563 | end 564 | # this will override the base classes initialize_versions option, so we can catch it. 565 | a_manipulator.send(:define_method, :initialize_versions) do |*args| 566 | process_proxy.initialize_versions *args 567 | end 568 | 569 | process_proxy.should_receive(:process!).with('100x100').ordered 570 | process_proxy.should_receive(:initialize_versions).ordered 571 | 572 | 573 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :process => '100x100', :manipulator => a_manipulator) 574 | end 575 | 576 | end 577 | 578 | describe "an UploadedFile with no versions" do 579 | it "should not respond to version methods" do 580 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey) 581 | @file.should_not respond_to(:thumb) 582 | @file.should_not respond_to(:large) 583 | end 584 | end 585 | 586 | describe "an UploadedFile with versions with illegal names" do 587 | it "should raise an ArgumentError" do 588 | lambda do 589 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey, :versions => [ :thumb, :path ]) 590 | end.should raise_error(ArgumentError, 'path is an illegal name for an UploadColumn version.') 591 | end 592 | end 593 | 594 | describe "an UploadedFile with versions" do 595 | before do 596 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey, :versions => [ :thumb, :large ]) 597 | end 598 | 599 | it "should respond to version methods" do 600 | @file.should respond_to(:thumb) 601 | @file.should respond_to(:large) 602 | end 603 | 604 | it "should return an UploadedFile instance when a version method is called" do 605 | @file.thumb.should be_instance_of(UploadColumn::UploadedFile) 606 | @file.large.should be_instance_of(UploadColumn::UploadedFile) 607 | end 608 | end 609 | 610 | describe "all versions of uploaded files", :shared => true do 611 | it "should return the filename including the version" do 612 | @thumb.filename.should == "kerb-thumb.jpg" 613 | @large.filename.should == "kerb-large.jpg" 614 | end 615 | 616 | it "should return the basename without the version" do 617 | @thumb.basename.should == "kerb" 618 | @large.basename.should == "kerb" 619 | end 620 | 621 | it "should return the extension" do 622 | @thumb.extension.should == "jpg" 623 | @large.extension.should == "jpg" 624 | end 625 | 626 | it "should return the correct suffix" do 627 | @thumb.suffix.should == :thumb 628 | @large.suffix.should == :large 629 | end 630 | end 631 | 632 | describe "a version of an uploaded UploadedFile" do 633 | before do 634 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey, :versions => [ :thumb, :large ]) 635 | @thumb = @file.thumb 636 | @large = @file.large 637 | end 638 | 639 | it_should_behave_like "all versions of uploaded files" 640 | 641 | it "should not be empty" do 642 | @thumb.should_not be_empty 643 | @large.should_not be_empty 644 | end 645 | 646 | it "should return the correct filesize" do 647 | @thumb.size.should == 87582 648 | @large.size.should == 87582 649 | end 650 | 651 | it "should return the original filename" do 652 | @thumb.original_filename.should == "kerb.jpg" 653 | @large.original_filename.should == "kerb.jpg" 654 | end 655 | end 656 | 657 | describe "uploading a file with versions as a Hash" do 658 | 659 | it "should process the files with the manipulator" do 660 | 661 | process_proxy = mock('nothing in particular') 662 | a_manipulator = Module.new 663 | a_manipulator.send(:define_method, :process! ) do |stuff| 664 | # horrible abuse of Ruby's closures. This allows us to set expectations on the process_proxy 665 | # and if process! is called, the process_proxy will be adressed instead. 666 | process_proxy.process!(self.filename, stuff) 667 | end 668 | 669 | process_proxy.should_receive(:process!).with('kerb-thumb.jpg', '200x200') 670 | process_proxy.should_receive(:process!).with('kerb-large.jpg', '300x300') 671 | 672 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :manipulator => a_manipulator, :versions => { :thumb => '200x200', :large => '300x300' }) 673 | @thumb = @file.thumb 674 | @large = @file.large 675 | end 676 | 677 | end 678 | 679 | 680 | describe "an version of an UploadedFile with versions as a hash" do 681 | 682 | before(:each) do 683 | process_proxy = mock('nothing in particular') 684 | a_manipulator = Module.new 685 | a_manipulator.send(:define_method, :process! ) { |stuff| true } 686 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :manipulator => a_manipulator, :versions => { :thumb => '200x200', :large => '300x300' }) 687 | @thumb = @file.thumb 688 | @large = @file.large 689 | end 690 | 691 | it_should_behave_like "all versions of uploaded files" 692 | 693 | it "should not be empty" do 694 | @thumb.should_not be_empty 695 | @large.should_not be_empty 696 | end 697 | 698 | it "should return the original filename" do 699 | @thumb.original_filename.should == "kerb.jpg" 700 | @large.original_filename.should == "kerb.jpg" 701 | end 702 | 703 | end 704 | 705 | describe "a retrieved UploadedFile" do 706 | 707 | before do 708 | @file = UploadColumn::UploadedFile.retrieve('kerb.jpg', nil, :monkey) 709 | @file.stub!(:size).and_return(87582) 710 | end 711 | 712 | it_should_behave_like "all uploaded files" 713 | 714 | it "should not be new" do 715 | @file.should_not be_new_file 716 | end 717 | 718 | it "should not be a tempfile" do 719 | @file.should_not be_a_tempfile 720 | end 721 | 722 | it "should return the correct path" do 723 | @file.path.should match_path(public_path('monkey/kerb.jpg')) 724 | end 725 | end 726 | 727 | describe "a version of a retrieved UploadedFile" do 728 | 729 | before do 730 | @file = UploadColumn::UploadedFile.retrieve('kerb.jpg', nil, :monkey, :versions => [:thumb, :large]) 731 | @thumb = @file.thumb 732 | @large = @file.large 733 | end 734 | 735 | it_should_behave_like "all versions of uploaded files" 736 | 737 | it "should not be new" do 738 | @file.should_not be_new_file 739 | end 740 | 741 | it "should not be a tempfile" do 742 | @file.should_not be_a_tempfile 743 | end 744 | 745 | it "should return the correct path" do 746 | @thumb.path.should match_path(public_path('monkey/kerb-thumb.jpg')) 747 | @large.path.should match_path(public_path('monkey/kerb-large.jpg')) 748 | end 749 | 750 | # Since the files don't exist in fixtures/ it wouldn't make sense to test their size 751 | end 752 | 753 | describe "a version of a saved UploadedFile" do 754 | before do 755 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :monkey, :versions => [:thumb, :large]) 756 | @file.send(:save) 757 | @thumb = @file.thumb 758 | @large = @file.large 759 | end 760 | 761 | it_should_behave_like "all versions of uploaded files" 762 | 763 | it "should not be new" do 764 | @file.should_not be_new_file 765 | end 766 | 767 | it "should not be a tempfile" do 768 | @file.should_not be_a_tempfile 769 | end 770 | 771 | it "should return the correct path" do 772 | @thumb.path.should match_path('public', 'monkey', 'kerb-thumb.jpg') 773 | @large.path.should match_path('public', 'monkey', 'kerb-large.jpg') 774 | end 775 | end 776 | 777 | describe "opening a temporary UploadedFile" do 778 | 779 | it "should raise an error if the path is incorrectly formed" do 780 | lambda do 781 | @file = UploadColumn::UploadedFile.retrieve_temp(file_path('kerb.jpg')) 782 | end.should raise_error(UploadColumn::TemporaryPathMalformedError, "#{file_path('kerb.jpg')} is not a valid temporary path!") 783 | end 784 | 785 | it "should raise an error if its in a subdirectory" do 786 | lambda do 787 | @file = UploadColumn::UploadedFile.retrieve_temp('somefolder/1234.56789.1234/donkey.jpg;llama.png') 788 | end.should raise_error(UploadColumn::TemporaryPathMalformedError, "somefolder/1234.56789.1234/donkey.jpg;llama.png is not a valid temporary path!") 789 | end 790 | 791 | it "should raise an error if its relative" do 792 | lambda do 793 | @file = UploadColumn::UploadedFile.retrieve_temp('../1234.56789.1234/donkey.jpg;llama.png') 794 | end.should raise_error(UploadColumn::TemporaryPathMalformedError, "../1234.56789.1234/donkey.jpg;llama.png is not a valid temporary path!") 795 | end 796 | 797 | it "should raise an error if the filename is omitted" do 798 | lambda do 799 | @file = UploadColumn::UploadedFile.retrieve_temp('1234.56789.1234;llama.png') 800 | end.should raise_error(UploadColumn::TemporaryPathMalformedError, "1234.56789.1234;llama.png is not a valid temporary path!") 801 | end 802 | 803 | it "should not raise an error on nil" do 804 | lambda do 805 | @file = UploadColumn::UploadedFile.retrieve_temp(nil) 806 | end.should_not raise_error 807 | end 808 | 809 | it "should not raise an error on empty String" do 810 | lambda do 811 | @file = UploadColumn::UploadedFile.retrieve_temp('') 812 | end.should_not raise_error 813 | end 814 | end 815 | 816 | describe "a retrieved temporary UploadedFile" do 817 | 818 | before(:all) do 819 | FileUtils.mkdir_p(public_path('tmp/123455.1233.1233')) 820 | FileUtils.cp(file_path('kerb.jpg'), public_path('tmp/123455.1233.1233/kerb.jpg')) 821 | end 822 | 823 | before do 824 | @file = UploadColumn::UploadedFile.retrieve_temp('123455.1233.1233/kerb.jpg') 825 | end 826 | 827 | it_should_behave_like "all uploaded files" 828 | 829 | it "should not be new" do 830 | @file.should_not be_new_file 831 | end 832 | 833 | it "should be a tempfile" do 834 | @file.should be_a_tempfile 835 | end 836 | 837 | it "should return the correct path" do 838 | @file.path.should match_path('public', 'tmp', '123455.1233.1233', 'kerb.jpg') 839 | end 840 | 841 | after(:all) do 842 | FileUtils.rm_rf(PUBLIC) 843 | end 844 | end 845 | 846 | describe "a retrieved temporary UploadedFile with an appended original filename" do 847 | before(:all) do 848 | FileUtils.mkdir_p(public_path('tmp/123455.1233.1233')) 849 | FileUtils.cp(file_path('kerb.jpg'), public_path('tmp/123455.1233.1233/kerb.jpg')) 850 | end 851 | 852 | before do 853 | @file = UploadColumn::UploadedFile.retrieve_temp('123455.1233.1233/kerb.jpg;monkey.png') 854 | end 855 | 856 | it "should not be new" do 857 | @file.should_not be_new_file 858 | end 859 | 860 | it "should be a tempfile" do 861 | @file.should be_a_tempfile 862 | end 863 | 864 | it "should return the correct original filename" do 865 | @file.original_filename.should == "monkey.png" 866 | end 867 | 868 | it "should return the correct path" do 869 | @file.path.should match_path('public', 'tmp', '123455.1233.1233', 'kerb.jpg') 870 | end 871 | 872 | after(:all) do 873 | FileUtils.rm_rf(PUBLIC) 874 | end 875 | end 876 | 877 | describe "a version of a retrieved temporary UploadedFile" do 878 | 879 | before(:all) do 880 | FileUtils.mkdir_p(public_path('tmp/123455.1233.1233')) 881 | FileUtils.cp(file_path('kerb.jpg'), public_path('tmp/123455.1233.1233/kerb.jpg')) 882 | end 883 | 884 | before do 885 | @file = UploadColumn::UploadedFile.retrieve_temp('123455.1233.1233/kerb.jpg', nil, :monkey, :versions => [:thumb, :large]) 886 | @thumb = @file.thumb 887 | @large = @file.large 888 | end 889 | 890 | it_should_behave_like "all versions of uploaded files" 891 | 892 | it "should not be new" do 893 | @file.should_not be_new_file 894 | end 895 | 896 | it "should be a tempfile" do 897 | @file.should be_a_tempfile 898 | end 899 | 900 | it "should return the correct path" do 901 | @thumb.path.should match_path(public_path('tmp/123455.1233.1233/kerb-thumb.jpg')) 902 | @large.path.should match_path(public_path('tmp/123455.1233.1233/kerb-large.jpg')) 903 | end 904 | 905 | after(:all) do 906 | FileUtils.rm_rf(PUBLIC) 907 | end 908 | end 909 | 910 | describe "uploading a file with validate_integrity set to true" do 911 | 912 | it "should raise an error if no extensions are set" do 913 | lambda do 914 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, nil, :validate_integrity => true) 915 | end.should raise_error(UploadColumn::UploadError) 916 | end 917 | 918 | it "should not raise an error if the extension is in extensions" do 919 | lambda do 920 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, nil, :validate_integrity => true, :extensions => %w(jpg gif png)) 921 | end.should_not raise_error 922 | end 923 | 924 | it "should raise an error if the extension is not in extensions" do 925 | lambda do 926 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, nil, :validate_integrity => true, :extensions => %w(doc gif png)) 927 | end.should raise_error(UploadColumn::IntegrityError) 928 | end 929 | end 930 | 931 | describe "An UploadedFile with no web_root set" do 932 | it "should return the correct URL and to_s" do 933 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :versions => [:thumb, :large]) 934 | @file.send(:save) 935 | 936 | @file.url.should == "/donkey/kerb.jpg" 937 | @file.to_s.should == "/donkey/kerb.jpg" 938 | @file.thumb.url.should == "/donkey/kerb-thumb.jpg" 939 | @file.large.url.should == "/donkey/kerb-large.jpg" 940 | end 941 | end 942 | 943 | describe "An UploadedFile with no web_root set and MS style slashes in its relative path" do 944 | it "should return the correct URL and to_s" do 945 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :versions => [:thumb, :large]) 946 | 947 | @file.should_receive(:relative_path).at_least(:once).and_return('stylesheets\something\monkey\kerb.jpg') 948 | @file.thumb.should_receive(:relative_path).at_least(:once).and_return('stylesheets\something\monkey\kerb-thumb.jpg') 949 | @file.large.should_receive(:relative_path).at_least(:once).and_return('stylesheets\something\monkey\kerb-large.jpg') 950 | 951 | @file.send(:save) 952 | 953 | @file.url.should == "/stylesheets/something/monkey/kerb.jpg" 954 | @file.to_s.should == "/stylesheets/something/monkey/kerb.jpg" 955 | @file.thumb.url.should == "/stylesheets/something/monkey/kerb-thumb.jpg" 956 | @file.large.url.should == "/stylesheets/something/monkey/kerb-large.jpg" 957 | end 958 | end 959 | 960 | describe "An UploadedFile with an absolute web_root set" do 961 | it "should return the correct URL and to_s" do 962 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :web_root => 'http://ape.com', :versions => [:thumb, :large]) 963 | @file.send(:save) 964 | 965 | @file.url.should == "http://ape.com/donkey/kerb.jpg" 966 | @file.to_s.should == "http://ape.com/donkey/kerb.jpg" 967 | @file.thumb.url.should == "http://ape.com/donkey/kerb-thumb.jpg" 968 | @file.large.url.should == "http://ape.com/donkey/kerb-large.jpg" 969 | end 970 | end 971 | 972 | describe "An UploadedFile with an absolute web_root set and MS style slashes in its relative path" do 973 | it "should return the correct URL and to_s" do 974 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :web_root => 'http://ape.com', :versions => [:thumb, :large]) 975 | @file.should_receive(:relative_path).at_least(:once).and_return('stylesheets\something\monkey\kerb.jpg') 976 | @file.thumb.should_receive(:relative_path).at_least(:once).and_return('stylesheets\something\monkey\kerb-thumb.jpg') 977 | @file.large.should_receive(:relative_path).at_least(:once).and_return('stylesheets\something\monkey\kerb-large.jpg') 978 | 979 | @file.send(:save) 980 | 981 | @file.url.should == "http://ape.com/stylesheets/something/monkey/kerb.jpg" 982 | @file.to_s.should == "http://ape.com/stylesheets/something/monkey/kerb.jpg" 983 | @file.thumb.url.should == "http://ape.com/stylesheets/something/monkey/kerb-thumb.jpg" 984 | @file.large.url.should == "http://ape.com/stylesheets/something/monkey/kerb-large.jpg" 985 | end 986 | end 987 | 988 | describe "An UploadedFile with a web_root set" do 989 | it "should return the correct URL" do 990 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey, :web_root => '/ape', :versions => [:thumb, :large]) 991 | @file.send(:save) 992 | 993 | @file.url.should == "/ape/donkey/kerb.jpg" 994 | @file.to_s.should == "/ape/donkey/kerb.jpg" 995 | @file.thumb.url.should == "/ape/donkey/kerb-thumb.jpg" 996 | @file.large.url.should == "/ape/donkey/kerb-large.jpg" 997 | end 998 | end 999 | 1000 | describe "the temp_value of an UploadedFile without an original filename" do 1001 | 1002 | setup do 1003 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey) 1004 | @file.should_receive(:original_filename).and_return(nil) 1005 | end 1006 | 1007 | it "should match the TempValueRegexp" do 1008 | @file.temp_value.should match(::UploadColumn::TempValueRegexp) 1009 | end 1010 | 1011 | it "should end in the filename" do 1012 | @file.temp_value.should match(/\/kerb\.jpg$/) 1013 | end 1014 | end 1015 | 1016 | describe "the temp_value of an UploadedFile with a different orignal filename" do 1017 | 1018 | setup do 1019 | @file = UploadColumn::UploadedFile.upload(stub_file('kerb.jpg'), nil, :donkey) 1020 | @file.should_receive(:original_filename).at_least(:once).and_return('monkey.png') 1021 | end 1022 | 1023 | it "should match the TempValueRegexp" do 1024 | @file.temp_value.should match(::UploadColumn::TempValueRegexp) 1025 | end 1026 | 1027 | it "should append the original_filename" do 1028 | @file.temp_value.should match(/kerb\.jpg;monkey\.png$/) 1029 | end 1030 | end 1031 | 1032 | describe "the temp_value of a retrieved temporary UploadedFile" do 1033 | 1034 | setup do 1035 | @file = UploadColumn::UploadedFile.retrieve_temp('12345.1234.12345/kerb.jpg', nil, :donkey) 1036 | @file.should_receive(:original_filename).at_least(:once).and_return(nil) 1037 | end 1038 | 1039 | it "should be mainatained" do 1040 | @file.temp_value.should == '12345.1234.12345/kerb.jpg' 1041 | end 1042 | end 1043 | 1044 | describe "the temp_value of an UploadedFile that is not temporary" do 1045 | 1046 | setup do 1047 | @file = UploadColumn::UploadedFile.retrieve('kerb.jpg', nil, :donkey) 1048 | end 1049 | 1050 | it "should be mainatained" do 1051 | @file.temp_value.should be_nil 1052 | end 1053 | end --------------------------------------------------------------------------------