├── spec ├── fixtures │ ├── case.JPG │ ├── new.jpeg │ ├── old.jpeg │ ├── test.jpeg │ ├── test.jpg │ ├── Uppercase.jpg │ ├── sponsored.doc │ ├── portrait.jpg │ ├── landscape.jpg │ ├── multi_page.pdf │ ├── bork.txt │ ├── new.txt │ ├── old.txt │ ├── bork.ttxt │ └── bork.txtt ├── storage │ └── fog_spec.rb ├── uploader │ ├── paths_spec.rb │ ├── mountable_spec.rb │ ├── callback_spec.rb │ ├── proxy_spec.rb │ ├── remove_spec.rb │ ├── default_url_spec.rb │ ├── overrides_spec.rb │ ├── extension_blacklist_spec.rb │ ├── extension_whitelist_spec.rb │ ├── configuration_spec.rb │ ├── download_spec.rb │ ├── processing_spec.rb │ ├── url_spec.rb │ └── cache_spec.rb ├── fog_credentials.rb ├── compatibility │ └── paperclip_spec.rb ├── processing │ ├── mime_types_spec.rb │ ├── mini_magick_spec.rb │ └── rmagick_spec.rb └── spec_helper.rb ├── features ├── fixtures │ ├── bork.txt │ ├── monkey.txt │ └── upcased_bork.txt ├── step_definitions │ ├── download_steps.rb │ ├── caching_steps.rb │ ├── store_steps.rb │ ├── activerecord_steps.rb │ ├── mount_steps.rb │ ├── datamapper_steps.rb │ ├── file_steps.rb │ └── general_steps.rb ├── support │ ├── env.rb │ └── activerecord.rb ├── download.feature ├── caching.feature ├── file_storage.feature ├── file_storage_overridden_filename.feature ├── file_storage_overridden_store_dir.feature ├── versions_caching_from_versions.feature ├── mount_activerecord.feature ├── file_storage_reversing_processor.feature ├── versions_overriden_store_dir.feature ├── versions_basics.feature ├── versions_overridden_filename.feature └── versions_nested_versions.feature ├── Gemfile ├── lib ├── carrierwave │ ├── version.rb │ ├── processing.rb │ ├── storage.rb │ ├── error.rb │ ├── uploader │ │ ├── default_url.rb │ │ ├── remove.rb │ │ ├── serialization.rb │ │ ├── callbacks.rb │ │ ├── url.rb │ │ ├── mountable.rb │ │ ├── extension_blacklist.rb │ │ ├── extension_whitelist.rb │ │ ├── proxy.rb │ │ ├── download.rb │ │ ├── processing.rb │ │ ├── store.rb │ │ ├── configuration.rb │ │ ├── cache.rb │ │ └── versions.rb │ ├── storage │ │ ├── abstract.rb │ │ └── file.rb │ ├── locale │ │ └── en.yml │ ├── processing │ │ ├── mime_types.rb │ │ └── mini_magick.rb │ ├── uploader.rb │ ├── orm │ │ └── activerecord.rb │ ├── validations │ │ └── active_model.rb │ ├── compatibility │ │ └── paperclip.rb │ ├── sanitized_file.rb │ └── test │ │ └── matchers.rb ├── generators │ ├── uploader_generator.rb │ └── templates │ │ └── uploader.rb └── carrierwave.rb ├── cucumber.yml ├── gemfiles ├── rails3_2.gemfile └── rails_master.gemfile ├── .gitignore ├── script ├── destroy ├── generate └── console ├── .travis.yml ├── Rakefile ├── CONTRIBUTING.md └── carrierwave.gemspec /spec/fixtures/case.JPG: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/new.jpeg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/old.jpeg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/test.jpeg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/test.jpg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /features/fixtures/bork.txt: -------------------------------------------------------------------------------- 1 | this is a file -------------------------------------------------------------------------------- /spec/fixtures/Uppercase.jpg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/sponsored.doc: -------------------------------------------------------------------------------- 1 | Hi there 2 | -------------------------------------------------------------------------------- /features/fixtures/monkey.txt: -------------------------------------------------------------------------------- 1 | this is another file -------------------------------------------------------------------------------- /features/fixtures/upcased_bork.txt: -------------------------------------------------------------------------------- 1 | THIS IS A FILE -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/carrierwave/version.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | VERSION = "0.7.1" 3 | end 4 | -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --format pretty --no-source 2 | html: --format html --out features.html -------------------------------------------------------------------------------- /spec/fixtures/portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/carrierwave/master/spec/fixtures/portrait.jpg -------------------------------------------------------------------------------- /spec/fixtures/landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/carrierwave/master/spec/fixtures/landscape.jpg -------------------------------------------------------------------------------- /spec/fixtures/multi_page.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/carrierwave/master/spec/fixtures/multi_page.pdf -------------------------------------------------------------------------------- /spec/storage/fog_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | for credential in FOG_CREDENTIALS 6 | fog_tests(credential) 7 | end 8 | -------------------------------------------------------------------------------- /lib/carrierwave/processing.rb: -------------------------------------------------------------------------------- 1 | require "carrierwave/processing/rmagick" 2 | require "carrierwave/processing/mini_magick" 3 | require "carrierwave/processing/mime_types" 4 | -------------------------------------------------------------------------------- /gemfiles/rails3_2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", :git => "https://github.com/rails/rails.git", :branch => "3-2-stable" 4 | gem "carrierwave", :path => "../" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /lib/carrierwave/storage.rb: -------------------------------------------------------------------------------- 1 | require "carrierwave/storage/abstract" 2 | require "carrierwave/storage/file" 3 | 4 | begin 5 | require "fog" 6 | rescue LoadError 7 | end 8 | 9 | require "carrierwave/storage/fog" if defined?(Fog) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | .yardoc 3 | .DS_Store 4 | spec/public 5 | pkg 6 | doc 7 | more/activerecord/spec/db 8 | more/activerecord/spec/public 9 | more/datamapper/spec/public 10 | *.project 11 | spec/test.log 12 | *.swp 13 | .rvmrc 14 | .bundle 15 | Gemfile.lock -------------------------------------------------------------------------------- /lib/generators/uploader_generator.rb: -------------------------------------------------------------------------------- 1 | class UploaderGenerator < Rails::Generators::NamedBase 2 | source_root File.expand_path("../templates", __FILE__) 3 | 4 | def create_uploader_file 5 | template "uploader.rb", "app/uploaders/#{file_name}_uploader.rb" 6 | end 7 | end -------------------------------------------------------------------------------- /lib/carrierwave/error.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | class UploadError < StandardError; end 3 | class IntegrityError < UploadError; end 4 | class InvalidParameter < UploadError; end 5 | class ProcessingError < UploadError; end 6 | class DownloadError < UploadError; end 7 | end 8 | -------------------------------------------------------------------------------- /gemfiles/rails_master.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", :github => "rails/rails", :branch => "master" 4 | gem "activerecord-deprecated_finders", :github => "rails/activerecord-deprecated_finders" 5 | gem "journey", :github => "rails/journey" 6 | gem "carrierwave", :path => "../" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /features/step_definitions/download_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I download the file '([^']+)'/ do |url| 2 | unless ENV['REMOTE'] == 'true' 3 | sham_rack_app = ShamRack.at('s3.amazonaws.com').stub 4 | sham_rack_app.register_resource('/Monkey/testfile.txt', 'S3 Remote File', 'text/plain') 5 | end 6 | 7 | @uploader.download!(url) 8 | 9 | unless ENV['REMOTE'] == 'true' 10 | ShamRack.unmount_all 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 3 | 4 | begin 5 | require 'rubigen' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'rubigen' 9 | end 10 | require 'rubigen/scripts/destroy' 11 | 12 | ARGV.shift if ['--help', '-h'].include?(ARGV[0]) 13 | RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit] 14 | RubiGen::Scripts::Destroy.new.run(ARGV) 15 | -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 3 | 4 | begin 5 | require 'rubigen' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'rubigen' 9 | end 10 | require 'rubigen/scripts/generate' 11 | 12 | ARGV.shift if ['--help', '-h'].include?(ARGV[0]) 13 | RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit] 14 | RubiGen::Scripts::Generate.new.run(ARGV) 15 | -------------------------------------------------------------------------------- /spec/fixtures/bork.txt: -------------------------------------------------------------------------------- 1 | bork bork bork Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /spec/fixtures/new.txt: -------------------------------------------------------------------------------- 1 | bork bork bork Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /spec/fixtures/old.txt: -------------------------------------------------------------------------------- 1 | bork bork bork Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /spec/fixtures/bork.ttxt: -------------------------------------------------------------------------------- 1 | bork bork bork Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /spec/fixtures/bork.txtt: -------------------------------------------------------------------------------- 1 | bork bork bork Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /lib/carrierwave/uploader/default_url.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module DefaultUrl 6 | 7 | def url(*args) 8 | super || default_url 9 | end 10 | 11 | ## 12 | # Override this method in your uploader to provide a default url 13 | # in case no file has been cached/stored yet. 14 | # 15 | def default_url; end 16 | 17 | end # DefaultPath 18 | end # Uploader 19 | end # CarrierWave -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # File: script/console 3 | irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' 4 | 5 | libs = " -r irb/completion" 6 | # Perhaps use a console_lib to store any extra methods I may want available in the cosole 7 | # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}" 8 | libs << " -r #{File.dirname(__FILE__) + '/../lib/carrierwave.rb'}" 9 | puts "Loading carrierwave gem" 10 | exec "#{irb} #{libs} --simple-prompt" -------------------------------------------------------------------------------- /features/step_definitions/caching_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Given /^the file '(.*?)' is cached file at '(.*?)'$/ do |file, cached| 4 | FileUtils.mkdir_p(File.dirname(file_path(cached))) 5 | FileUtils.cp(file_path(file), file_path(cached)) 6 | end 7 | 8 | When /^I cache the file '(.*?)'$/ do |file| 9 | @uploader.cache!(File.open(file_path(file))) 10 | end 11 | 12 | When /^I retrieve the cache name '(.*?)' from the cache$/ do |name| 13 | @uploader.retrieve_from_cache!(name) 14 | end -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $:.unshift File.expand_path(File.join('..', '..', 'lib'), File.dirname(__FILE__)) 4 | 5 | require File.join(File.dirname(__FILE__), 'activerecord') 6 | 7 | require 'rspec' 8 | require 'carrierwave' 9 | require 'sham_rack' 10 | 11 | alias :running :lambda 12 | 13 | def file_path( *paths ) 14 | File.expand_path(File.join('..', *paths), File.dirname(__FILE__)) 15 | end 16 | 17 | CarrierWave.root = file_path('public') 18 | 19 | After do 20 | FileUtils.rm_rf(file_path("public")) 21 | end 22 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/remove.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Remove 6 | extend ActiveSupport::Concern 7 | 8 | include CarrierWave::Uploader::Callbacks 9 | 10 | ## 11 | # Removes the file and reset it 12 | # 13 | def remove! 14 | with_callbacks(:remove) do 15 | @file.delete if @file 16 | @file = nil 17 | @cache_id = nil 18 | end 19 | end 20 | 21 | end # Remove 22 | end # Uploader 23 | end # CarrierWave 24 | -------------------------------------------------------------------------------- /features/step_definitions/store_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Given /^the file '(.*?)' is stored at '(.*?)'$/ do |file, stored| 4 | FileUtils.mkdir_p(File.dirname(file_path(stored))) 5 | FileUtils.cp(file_path(file), file_path(stored)) 6 | end 7 | 8 | When /^I store the file$/ do 9 | @uploader.store! 10 | end 11 | 12 | When /^I store the file '(.*?)'$/ do |file| 13 | @uploader.store!(File.open(file_path(file))) 14 | end 15 | 16 | When /^I retrieve the file '(.*?)' from the store$/ do |identifier| 17 | @uploader.retrieve_from_store!(identifier) 18 | end 19 | -------------------------------------------------------------------------------- /features/step_definitions/activerecord_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Given /^an activerecord class that uses the '([^\']*)' table$/ do |name| 4 | @mountee_klass = Class.new(ActiveRecord::Base) 5 | @mountee_klass.table_name = name 6 | end 7 | 8 | Given /^an instance of the activerecord class$/ do 9 | @instance = @mountee_klass.new 10 | end 11 | 12 | When /^I save the active record$/ do 13 | @instance.save! 14 | end 15 | 16 | When /^I reload the active record$/ do 17 | @instance = @instance.class.find(@instance.id) 18 | end 19 | 20 | When /^I delete the active record$/ do 21 | @instance.destroy 22 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - gem install bundler 3 | 4 | notifications: 5 | email: false 6 | 7 | rvm: 8 | - 1.8.7 9 | - 1.9.2 10 | - 1.9.3 11 | - 2.0.0 12 | - ree 13 | 14 | gemfile: 15 | - gemfiles/rails3_2.gemfile 16 | - gemfiles/rails_master.gemfile 17 | 18 | matrix: 19 | allow_failures: 20 | - rvm: 2.0.0 21 | exclude: 22 | - rvm: 1.8.7 23 | gemfile: gemfiles/rails_master.gemfile 24 | - rvm: 1.9.2 25 | gemfile: gemfiles/rails_master.gemfile 26 | - rvm: ree 27 | gemfile: gemfiles/rails_master.gemfile 28 | 29 | before_script: 30 | - "mysql -e 'create database carrierwave_test;'" 31 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/abstract.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Storage 5 | 6 | ## 7 | # This file serves mostly as a specification for Storage engines. There is no requirement 8 | # that storage engines must be a subclass of this class. 9 | # 10 | class Abstract 11 | 12 | attr_reader :uploader 13 | 14 | def initialize(uploader) 15 | @uploader = uploader 16 | end 17 | 18 | def identifier 19 | uploader.filename 20 | end 21 | 22 | def store!(file) 23 | end 24 | 25 | def retrieve!(identifier) 26 | end 27 | 28 | end # Abstract 29 | end # Storage 30 | end # CarrierWave 31 | -------------------------------------------------------------------------------- /spec/uploader/paths_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @root = CarrierWave.root 9 | CarrierWave.root = nil 10 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 11 | @uploader = @uploader_class.new 12 | end 13 | 14 | after do 15 | FileUtils.rm_rf(public_path) 16 | CarrierWave.root = @root 17 | end 18 | 19 | describe '#root' do 20 | it "should default to the current value of CarrierWave.root" do 21 | @uploader.root.should be_nil 22 | CarrierWave.root = public_path 23 | @uploader.root.should == public_path 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'bundler' 10 | Bundler::GemHelper.install_tasks 11 | 12 | require 'rake' 13 | require 'rspec/core/rake_task' 14 | require 'cucumber' 15 | require 'cucumber/rake/task' 16 | 17 | desc "Run all examples" 18 | RSpec::Core::RakeTask.new(:spec) do |t| 19 | t.rspec_opts = %w[--color] 20 | end 21 | 22 | desc "Run cucumber features" 23 | Cucumber::Rake::Task.new(:features) do |t| 24 | t.cucumber_opts = "features --format progress" 25 | end 26 | 27 | task :default => [:spec, :features] 28 | -------------------------------------------------------------------------------- /features/step_definitions/mount_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | When /^I assign the file '([^\']*)' to the '([^\']*)' column$/ do |path, column| 4 | @instance.send("#{column}=", File.open(file_path(path))) 5 | end 6 | 7 | Given /^the uploader class is mounted on the '([^\']*)' column$/ do |column| 8 | @mountee_klass.mount_uploader column.to_sym, @klass 9 | end 10 | 11 | When /^I retrieve the file later from the cache name for the column '([^\']*)'$/ do |column| 12 | new_instance = @instance.class.new 13 | new_instance.send("#{column}_cache=", @instance.send("#{column}_cache")) 14 | @instance = new_instance 15 | end 16 | 17 | Then /^the url for the column '([^\']*)' should be '([^\']*)'$/ do |column, url| 18 | @instance.send("#{column}_url").should == url 19 | end 20 | -------------------------------------------------------------------------------- /features/support/activerecord.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'mysql2' 4 | 5 | require 'active_record' 6 | require 'carrierwave/mount' 7 | require 'carrierwave/orm/activerecord' 8 | 9 | # Change this if MySQL is unavailable 10 | dbconfig = { 11 | :adapter => 'mysql2', 12 | :database => 'carrierwave_test', 13 | :username => 'root', 14 | :encoding => 'utf8' 15 | } 16 | 17 | ActiveRecord::Base.establish_connection(dbconfig) 18 | ActiveRecord::Migration.verbose = false 19 | 20 | class TestMigration < ActiveRecord::Migration 21 | def self.up 22 | create_table :users, :force => true do |t| 23 | t.column :avatar, :string 24 | end 25 | end 26 | 27 | def self.down 28 | drop_table :users 29 | end 30 | end 31 | 32 | Before do 33 | TestMigration.up 34 | end 35 | -------------------------------------------------------------------------------- /features/step_definitions/datamapper_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Given /^a datamapper class that has a '([^\']*)' column$/ do |column| 4 | @mountee_klass = Class.new do 5 | include DataMapper::Resource 6 | 7 | storage_names[:default] = 'users' 8 | 9 | property :id, DataMapper::Types::Serial 10 | property column.to_sym, String 11 | end 12 | @mountee_klass.auto_migrate! 13 | end 14 | 15 | Given /^an instance of the datamapper class$/ do 16 | @instance = @mountee_klass.new 17 | end 18 | 19 | When /^I save the datamapper record$/ do 20 | @instance.save 21 | end 22 | 23 | When /^I reload the datamapper record$/ do 24 | @instance = @instance.class.first(:id => @instance.key) 25 | end 26 | 27 | When /^I delete the datamapper record$/ do 28 | @instance.destroy 29 | end 30 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/serialization.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "active_support/json" 4 | require "active_support/core_ext/hash" 5 | 6 | module CarrierWave 7 | module Uploader 8 | module Serialization 9 | extend ActiveSupport::Concern 10 | 11 | def serializable_hash 12 | {"url" => url}.merge Hash[versions.map { |name, version| [name, { "url" => version.url }] }] 13 | end 14 | 15 | def as_json(options=nil) 16 | Hash[mounted_as || "uploader", serializable_hash] 17 | end 18 | 19 | def to_json 20 | ActiveSupport::JSON.encode(as_json) 21 | end 22 | 23 | def to_xml(options={}) 24 | merged_options = options.merge(:root => mounted_as || "uploader", :type => 'uploader') 25 | serializable_hash.to_xml(merged_options) 26 | end 27 | 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/carrierwave/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | carrierwave_processing_error: failed to be processed 5 | carrierwave_integrity_error: is not of an allowed file type 6 | carrierwave_download_error: could not be downloaded 7 | extension_white_list_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}" 8 | extension_black_list_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}" 9 | rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image? Original Error: %{e}" 10 | mime_types_processing_error: "Failed to process file with MIME::Types, maybe not valid content-type? Original Error: %{e}" 11 | mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}" 12 | -------------------------------------------------------------------------------- /spec/uploader/mountable_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe '#model' do 17 | it "should be remembered from initialization" do 18 | model = mock('a model object') 19 | @uploader = @uploader_class.new(model) 20 | @uploader.model.should == model 21 | end 22 | end 23 | 24 | describe '#mounted_as' do 25 | it "should be remembered from initialization" do 26 | model = mock('a model object') 27 | @uploader = @uploader_class.new(model, :llama) 28 | @uploader.model.should == model 29 | @uploader.mounted_as.should == :llama 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /spec/uploader/callback_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | it "should keep callbacks on different classes isolated" do 8 | @uploader_class_1 = Class.new(CarrierWave::Uploader::Base) 9 | 10 | # First Uploader only has default before-callback 11 | @uploader_class_1._before_callbacks[:cache].should == [:check_whitelist!, :check_blacklist!] 12 | 13 | @uploader_class_2 = Class.new(CarrierWave::Uploader::Base) 14 | @uploader_class_2.before :cache, :before_cache_callback 15 | 16 | # Second Uploader defined with another callback 17 | @uploader_class_2._before_callbacks[:cache].should == [:check_whitelist!, :check_blacklist!, :before_cache_callback] 18 | 19 | # Make sure the first Uploader doesn't inherit the same callback 20 | @uploader_class_1._before_callbacks[:cache].should == [:check_whitelist!, :check_blacklist!] 21 | end 22 | 23 | 24 | end 25 | -------------------------------------------------------------------------------- /features/download.feature: -------------------------------------------------------------------------------- 1 | Feature: downloading files 2 | In order to allow users to upload remote files 3 | As a developer using CarrierWave 4 | I want to download files to the filesystem via HTTP 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And an instance of that class 9 | 10 | Scenario: download a file 11 | When I download the file 'http://s3.amazonaws.com/Monkey/testfile.txt' 12 | Then there should be a file called 'testfile.txt' somewhere in a subdirectory of 'public/uploads/tmp' 13 | And the file called 'testfile.txt' in a subdirectory of 'public/uploads/tmp' should contain 'S3 Remote File' 14 | 15 | Scenario: downloading a file then storing 16 | When I download the file 'http://s3.amazonaws.com/Monkey/testfile.txt' 17 | And I store the file 18 | Then there should be a file at 'public/uploads/testfile.txt' 19 | And the file at 'public/uploads/testfile.txt' should contain 'S3 Remote File' 20 | 21 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/callbacks.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Callbacks 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | class_attribute :_before_callbacks, :_after_callbacks, 10 | :instance_writer => false 11 | self._before_callbacks = Hash.new [] 12 | self._after_callbacks = Hash.new [] 13 | end 14 | 15 | def with_callbacks(kind, *args) 16 | self.class._before_callbacks[kind].each { |c| send c, *args } 17 | yield 18 | self.class._after_callbacks[kind].each { |c| send c, *args } 19 | end 20 | 21 | module ClassMethods 22 | def before(kind, callback) 23 | self._before_callbacks = self._before_callbacks. 24 | merge kind => _before_callbacks[kind] + [callback] 25 | end 26 | 27 | def after(kind, callback) 28 | self._after_callbacks = self._after_callbacks. 29 | merge kind => _after_callbacks[kind] + [callback] 30 | end 31 | end # ClassMethods 32 | 33 | end # Callbacks 34 | end # Uploader 35 | end # CarrierWave 36 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/url.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Url 6 | extend ActiveSupport::Concern 7 | include CarrierWave::Uploader::Configuration 8 | 9 | ## 10 | # === Parameters 11 | # 12 | # [Hash] optional, the query params (only AWS) 13 | # 14 | # === Returns 15 | # 16 | # [String] the location where this file is accessible via a url 17 | # 18 | def url(options = {}) 19 | if file.respond_to?(:url) and not file.url.blank? 20 | file.method(:url).arity == 0 ? file.url : file.url(options) 21 | elsif file.respond_to?(:path) 22 | path = file.path.gsub(File.expand_path(root), '') 23 | 24 | if host = asset_host 25 | if host.respond_to? :call 26 | "#{host.call(file)}#{path}" 27 | else 28 | "#{host}#{path}" 29 | end 30 | else 31 | (base_path || "") + path 32 | end 33 | end 34 | end 35 | 36 | def to_s 37 | url || '' 38 | end 39 | 40 | end # Url 41 | end # Uploader 42 | end # CarrierWave 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CarrierWave 2 | 3 | CarrierWave thrives on a large number of [contributors](https://github.com/jnicklas/carrierwave/contributors), 4 | and pull requests are very welcome. Before submitting a pull request, please make sure that your changes are well tested. 5 | 6 | First, make sure you have `imagemagick` and `ghostscript` installed. 7 | 8 | Then, you'll need to install bundler and the gem dependencies: 9 | 10 | gem install bundler 11 | bundle install 12 | 13 | You should now be able to run the local tests: 14 | 15 | bundle exec rake 16 | 17 | You can also run the remote specs by creating a ~/.fog file: 18 | 19 | ```yaml 20 | :carrierwave: 21 | :aws_access_key_id: xxx 22 | :aws_secret_access_key: yyy 23 | :rackspace_username: xxx 24 | :rackspace_api_key: yyy 25 | :google_storage_access_key_id: xxx 26 | :google_storage_secret_access_key: yyy 27 | ``` 28 | 29 | You should now be able to run the remote tests: 30 | 31 | REMOTE=true bundle exec rake 32 | 33 | Please test with the latest Ruby 1.8.x and 1.9.x versions using RVM if possible. 34 | 35 | ## Running active record tests 36 | 37 | Make sure you have a local MySQL database named `carrierwave_test` with the username 38 | `root` and empty password. 39 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/mountable.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Mountable 6 | 7 | attr_reader :model, :mounted_as 8 | 9 | ## 10 | # If a model is given as the first parameter, it will be stored in the uploader, and 11 | # available throught +#model+. Likewise, mounted_as stores the name of the column 12 | # where this instance of the uploader is mounted. These values can then be used inside 13 | # your uploader. 14 | # 15 | # If you do not wish to mount your uploaders with the ORM extensions in -more then you 16 | # can override this method inside your uploader. Just be sure to call +super+ 17 | # 18 | # === Parameters 19 | # 20 | # [model (Object)] Any kind of model object 21 | # [mounted_as (Symbol)] The name of the column where this uploader is mounted 22 | # 23 | # === Examples 24 | # 25 | # class MyUploader < CarrierWave::Uploader::Base 26 | # 27 | # def store_dir 28 | # File.join('public', 'files', mounted_as, model.permalink) 29 | # end 30 | # end 31 | # 32 | def initialize(model=nil, mounted_as=nil) 33 | @model = model 34 | @mounted_as = mounted_as 35 | end 36 | 37 | end # Mountable 38 | end # Uploader 39 | end # CarrierWave 40 | -------------------------------------------------------------------------------- /spec/fog_credentials.rb: -------------------------------------------------------------------------------- 1 | unless defined?(FOG_CREDENTIALS) 2 | 3 | credentials = [] 4 | 5 | if Fog.mocking? 6 | # Local and Rackspace don't have fog mock support yet 7 | mappings = { 8 | 'AWS' => [:aws_access_key_id, :aws_secret_access_key], 9 | 'Google' => [:google_storage_access_key_id, :google_storage_secret_access_key], 10 | # 'Local' => [:local_root], 11 | # 'Rackspace' => [:rackspace_api_key, :rackspace_username] 12 | } 13 | 14 | for provider, keys in mappings 15 | data = {:provider => provider} 16 | for key in keys 17 | data[key] = key.to_s 18 | end 19 | credentials << data 20 | end 21 | 22 | FOG_CREDENTIALS = credentials 23 | else 24 | Fog.credential = :carrierwave 25 | 26 | mappings = { 27 | 'AWS' => [:aws_access_key_id, :aws_secret_access_key], 28 | 'Google' => [:google_storage_access_key_id, :google_storage_secret_access_key], 29 | 'Local' => [:local_root], 30 | 'Rackspace' => [:rackspace_api_key, :rackspace_username] 31 | } 32 | 33 | for provider, keys in mappings 34 | unless (creds = Fog.credentials.reject {|key, value| ![*keys].include?(key)}).empty? 35 | data = {:provider => provider} 36 | for key in keys 37 | data[key] = creds[key] 38 | end 39 | credentials << data 40 | end 41 | end 42 | 43 | FOG_CREDENTIALS = credentials 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/uploader/proxy_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe '#blank?' do 17 | it "should be true when nothing has been done" do 18 | @uploader.should be_blank 19 | end 20 | 21 | it "should not be true when the file is empty" do 22 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 23 | @uploader.should be_blank 24 | end 25 | 26 | it "should not be true when a file has been cached" do 27 | @uploader.cache!(File.open(file_path('test.jpg'))) 28 | @uploader.should_not be_blank 29 | end 30 | end 31 | 32 | describe '#read' do 33 | it "should be nil by default" do 34 | @uploader.read.should be_nil 35 | end 36 | 37 | it "should read the contents of a cached file" do 38 | @uploader.cache!(File.open(file_path('test.jpg'))) 39 | @uploader.read.should == "this is stuff" 40 | end 41 | end 42 | 43 | describe '#size' do 44 | it "should be zero by default" do 45 | @uploader.size.should == 0 46 | end 47 | 48 | it "should get the size of a cached file" do 49 | @uploader.cache!(File.open(file_path('test.jpg'))) 50 | @uploader.size.should == 13 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /carrierwave.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require 'carrierwave/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "carrierwave" 9 | s.version = CarrierWave::VERSION 10 | 11 | s.authors = ["Jonas Nicklas"] 12 | s.date = Date.today 13 | s.description = "Upload files in your Ruby applications, map them to a range of ORMs, store them on different backends." 14 | s.summary = "Ruby file upload library" 15 | s.email = ["jonas.nicklas@gmail.com"] 16 | s.extra_rdoc_files = ["README.md"] 17 | s.files = Dir.glob("{bin,lib}/**/*") + %w(README.md) 18 | s.homepage = %q{https://github.com/jnicklas/carrierwave} 19 | s.rdoc_options = ["--main"] 20 | s.require_paths = ["lib"] 21 | s.rubyforge_project = %q{carrierwave} 22 | s.rubygems_version = %q{1.3.5} 23 | s.specification_version = 3 24 | 25 | s.add_dependency "activesupport", ">= 3.2.0" 26 | s.add_dependency "activemodel", ">= 3.2.0" 27 | 28 | s.add_development_dependency "mysql2" 29 | s.add_development_dependency "rails", ">= 3.2.0" 30 | s.add_development_dependency "cucumber", "~> 1.1.4" 31 | s.add_development_dependency "json" 32 | s.add_development_dependency "rspec", "~> 2.12.0" 33 | s.add_development_dependency "sham_rack" 34 | s.add_development_dependency "timecop" 35 | s.add_development_dependency "fog", ">= 1.3.1" 36 | s.add_development_dependency "mini_magick" 37 | s.add_development_dependency "rmagick" 38 | end 39 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/extension_blacklist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module ExtensionBlacklist 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before :cache, :check_blacklist! 8 | end 9 | 10 | ## 11 | # Override this method in your uploader to provide a black list of extensions which 12 | # are prohibited to be uploaded. Compares the file's extension case insensitive. 13 | # Furthermore, not only strings but Regexp are allowed as well. 14 | # 15 | # When using a Regexp in the black list, `\A` and `\z` are automatically added to 16 | # the Regexp expression, also case insensitive. 17 | # 18 | # === Returns 19 | 20 | # [NilClass, Array[String,Regexp]] a black list of extensions which are prohibited to be uploaded 21 | # 22 | # === Examples 23 | # 24 | # def extension_black_list 25 | # %w(swf tiff) 26 | # end 27 | # 28 | # Basically the same, but using a Regexp: 29 | # 30 | # def extension_black_list 31 | # [/swf/, 'tiff'] 32 | # end 33 | # 34 | 35 | def extension_black_list; end 36 | 37 | private 38 | 39 | def check_blacklist!(new_file) 40 | extension = new_file.extension.to_s 41 | if extension_black_list and extension_black_list.detect { |item| extension =~ /\A#{item}\z/i } 42 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_black_list_error", :extension => new_file.extension.inspect, :prohibited_types => extension_black_list.join(", ")) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/extension_whitelist.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module ExtensionWhitelist 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | before :cache, :check_whitelist! 10 | end 11 | 12 | ## 13 | # Override this method in your uploader to provide a white list of extensions which 14 | # are allowed to be uploaded. Compares the file's extension case insensitive. 15 | # Furthermore, not only strings but Regexp are allowed as well. 16 | # 17 | # When using a Regexp in the white list, `\A` and `\z` are automatically added to 18 | # the Regexp expression, also case insensitive. 19 | # 20 | # === Returns 21 | # 22 | # [NilClass, Array[String,Regexp]] a white list of extensions which are allowed to be uploaded 23 | # 24 | # === Examples 25 | # 26 | # def extension_white_list 27 | # %w(jpg jpeg gif png) 28 | # end 29 | # 30 | # Basically the same, but using a Regexp: 31 | # 32 | # def extension_white_list 33 | # [/jpe?g/, 'gif', 'png'] 34 | # end 35 | # 36 | def extension_white_list; end 37 | 38 | private 39 | 40 | def check_whitelist!(new_file) 41 | extension = new_file.extension.to_s 42 | if extension_white_list and not extension_white_list.detect { |item| extension =~ /\A#{item}\z/i } 43 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_white_list_error", :extension => new_file.extension.inspect, :allowed_types => extension_white_list.join(", ")) 44 | end 45 | end 46 | 47 | end # ExtensionWhitelist 48 | end # Uploader 49 | end # CarrierWave 50 | -------------------------------------------------------------------------------- /features/caching.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage 2 | In order to be able to temporarily store files to disk 3 | As a developer using CarrierWave 4 | I want to cache files 5 | 6 | Scenario: cache a file 7 | Given an uploader class that uses the 'file' storage 8 | And an instance of that class 9 | When I cache the file 'fixtures/bork.txt' 10 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 11 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 12 | 13 | Scenario: cache two files in succession 14 | Given an uploader class that uses the 'file' storage 15 | And an instance of that class 16 | When I cache the file 'fixtures/bork.txt' 17 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 18 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 19 | When I cache the file 'fixtures/monkey.txt' 20 | Then there should be a file called 'monkey.txt' somewhere in a subdirectory of 'public/uploads/tmp' 21 | And the file called 'monkey.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/monkey.txt' 22 | 23 | Scenario: retrieving a file from cache 24 | Given an uploader class that uses the 'file' storage 25 | And an instance of that class 26 | And the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 27 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 28 | Then the uploader should have 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' as its current path -------------------------------------------------------------------------------- /spec/compatibility/paperclip_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | require 'carrierwave/orm/activerecord' 6 | 7 | module Rails; end unless defined?(Rails) 8 | 9 | describe CarrierWave::Compatibility::Paperclip do 10 | 11 | before do 12 | Rails.stub(:root).and_return('/rails/root') 13 | Rails.stub(:env).and_return('test') 14 | @uploader_class = Class.new(CarrierWave::Uploader::Base) do 15 | include CarrierWave::Compatibility::Paperclip 16 | end 17 | @model = mock('a model') 18 | @model.stub!(:id).and_return(23) 19 | @uploader = @uploader_class.new(@model, :monkey) 20 | end 21 | 22 | after do 23 | FileUtils.rm_rf(public_path) 24 | end 25 | 26 | describe '#store_path' do 27 | it "should mimics paperclip default" do 28 | @uploader.store_path("monkey.png").should == "/rails/root/public/system/monkeys/23/original/monkey.png" 29 | end 30 | 31 | it "should interpolate the root path" do 32 | @uploader.stub!(:paperclip_path).and_return(":rails_root/foo/bar") 33 | @uploader.store_path("monkey.png").should == Rails.root + "/foo/bar" 34 | end 35 | 36 | it "should interpolate the attachment" do 37 | @uploader.stub!(:paperclip_path).and_return("/foo/:attachment/bar") 38 | @uploader.store_path("monkey.png").should == "/foo/monkeys/bar" 39 | end 40 | 41 | it "should interpolate the id" do 42 | @uploader.stub!(:paperclip_path).and_return("/foo/:id/bar") 43 | @uploader.store_path("monkey.png").should == "/foo/23/bar" 44 | end 45 | 46 | it "should interpolate the id partition" do 47 | @uploader.stub!(:paperclip_path).and_return("/foo/:id_partition/bar") 48 | @uploader.store_path("monkey.png").should == "/foo/000/000/023/bar" 49 | end 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/proxy.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Proxy 6 | 7 | ## 8 | # === Returns 9 | # 10 | # [Boolean] Whether the uploaded file is blank 11 | # 12 | def blank? 13 | file.blank? 14 | end 15 | 16 | ## 17 | # === Returns 18 | # 19 | # [String] the path where the file is currently located. 20 | # 21 | def current_path 22 | file.path if file.respond_to?(:path) 23 | end 24 | 25 | alias_method :path, :current_path 26 | 27 | ## 28 | # Returns a string that uniquely identifies the last stored file 29 | # 30 | # === Returns 31 | # 32 | # [String] uniquely identifies a file 33 | # 34 | def identifier 35 | storage.identifier if storage.respond_to?(:identifier) 36 | end 37 | 38 | ## 39 | # Read the contents of the file 40 | # 41 | # === Returns 42 | # 43 | # [String] contents of the file 44 | # 45 | def read 46 | file.read if file.respond_to?(:read) 47 | end 48 | 49 | ## 50 | # Fetches the size of the currently stored/cached file 51 | # 52 | # === Returns 53 | # 54 | # [Integer] size of the file 55 | # 56 | def size 57 | file.respond_to?(:size) ? file.size : 0 58 | end 59 | 60 | ## 61 | # Return the size of the file when asked for its length 62 | # 63 | # === Returns 64 | # 65 | # [Integer] size of the file 66 | # 67 | # === Note 68 | # 69 | # This was added because of the way Rails handles length/size validations in 3.0.6 and above. 70 | # 71 | def length 72 | size 73 | end 74 | 75 | end # Proxy 76 | end # Uploader 77 | end # CarrierWave 78 | -------------------------------------------------------------------------------- /lib/generators/templates/uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class <%= class_name %>Uploader < CarrierWave::Uploader::Base 4 | 5 | # Include RMagick or MiniMagick support: 6 | # include CarrierWave::RMagick 7 | # include CarrierWave::MiniMagick 8 | 9 | # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: 10 | # include Sprockets::Helpers::RailsHelper 11 | # include Sprockets::Helpers::IsolatedHelper 12 | 13 | # Choose what kind of storage to use for this uploader: 14 | storage :file 15 | # storage :fog 16 | 17 | # Override the directory where uploaded files will be stored. 18 | # This is a sensible default for uploaders that are meant to be mounted: 19 | def store_dir 20 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 21 | end 22 | 23 | # Provide a default URL as a default if there hasn't been a file uploaded: 24 | # def default_url 25 | # # For Rails 3.1+ asset pipeline compatibility: 26 | # # asset_path("fallback/" + [version_name, "default.png"].compact.join('_')) 27 | # 28 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_') 29 | # end 30 | 31 | # Process files as they are uploaded: 32 | # process :scale => [200, 300] 33 | # 34 | # def scale(width, height) 35 | # # do something 36 | # end 37 | 38 | # Create different versions of your uploaded files: 39 | # version :thumb do 40 | # process :scale => [50, 50] 41 | # end 42 | 43 | # Add a white list of extensions which are allowed to be uploaded. 44 | # For images you might use something like this: 45 | # def extension_white_list 46 | # %w(jpg jpeg gif png) 47 | # end 48 | 49 | # Override the filename of the uploaded files: 50 | # Avoid using model.id or version_name here, see uploader/store.rb for details. 51 | # def filename 52 | # "something.jpg" if original_filename 53 | # end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Storage 5 | 6 | ## 7 | # File storage stores file to the Filesystem (surprising, no?). There's really not much 8 | # to it, it uses the store_dir defined on the uploader as the storage location. That's 9 | # pretty much it. 10 | # 11 | class File < Abstract 12 | 13 | ## 14 | # Move the file to the uploader's store path. 15 | # 16 | # By default, store!() uses copy_to(), which operates by copying the file 17 | # from the cache to the store, then deleting the file from the cache. 18 | # If move_to_store() is overriden to return true, then store!() uses move_to(), 19 | # which simply moves the file from cache to store. Useful for large files. 20 | # 21 | # === Parameters 22 | # 23 | # [file (CarrierWave::SanitizedFile)] the file to store 24 | # 25 | # === Returns 26 | # 27 | # [CarrierWave::SanitizedFile] a sanitized file 28 | # 29 | def store!(file) 30 | path = ::File.expand_path(uploader.store_path, uploader.root) 31 | if uploader.move_to_store 32 | file.move_to(path, uploader.permissions, uploader.directory_permissions) 33 | else 34 | file.copy_to(path, uploader.permissions, uploader.directory_permissions) 35 | end 36 | end 37 | 38 | ## 39 | # Retrieve the file from its store path 40 | # 41 | # === Parameters 42 | # 43 | # [identifier (String)] the filename of the file 44 | # 45 | # === Returns 46 | # 47 | # [CarrierWave::SanitizedFile] a sanitized file 48 | # 49 | def retrieve!(identifier) 50 | path = ::File.expand_path(uploader.store_path(identifier), uploader.root) 51 | CarrierWave::SanitizedFile.new(path) 52 | end 53 | 54 | end # File 55 | end # Storage 56 | end # CarrierWave 57 | -------------------------------------------------------------------------------- /spec/uploader/remove_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe '#remove!' do 17 | before do 18 | @file = File.open(file_path('test.jpg')) 19 | 20 | @stored_file = mock('a stored file') 21 | @stored_file.stub!(:path).and_return('/path/to/somewhere') 22 | @stored_file.stub!(:url).and_return('http://www.example.com') 23 | @stored_file.stub!(:identifier).and_return('this-is-me') 24 | @stored_file.stub!(:delete) 25 | 26 | @storage = mock('a storage engine') 27 | @storage.stub!(:store!).and_return(@stored_file) 28 | 29 | @uploader_class.storage.stub!(:new).and_return(@storage) 30 | @uploader.store!(@file) 31 | end 32 | 33 | it "should reset the current path" do 34 | @uploader.remove! 35 | @uploader.current_path.should be_nil 36 | end 37 | 38 | it "should not be cached" do 39 | @uploader.remove! 40 | @uploader.should_not be_cached 41 | end 42 | 43 | it "should reset the url" do 44 | @uploader.cache!(@file) 45 | @uploader.remove! 46 | @uploader.url.should be_nil 47 | end 48 | 49 | it "should reset the identifier" do 50 | @uploader.remove! 51 | @uploader.identifier.should be_nil 52 | end 53 | 54 | it "should delete the file" do 55 | @stored_file.should_receive(:delete) 56 | @uploader.remove! 57 | end 58 | 59 | it "should reset the cache_name" do 60 | @uploader.cache!(@file) 61 | @uploader.remove! 62 | @uploader.cache_name.should be_nil 63 | end 64 | 65 | it "should do nothing when trying to remove an empty file" do 66 | running { @uploader.remove! }.should_not raise_error 67 | end 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /features/step_definitions/file_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | ### 4 | # EXISTENCE 5 | 6 | Then /^there should be a file at '(.*?)'$/ do |file| 7 | File.exist?(file_path(file)).should be_true 8 | end 9 | 10 | Then /^there should not be a file at '(.*?)'$/ do |file| 11 | File.exist?(file_path(file)).should be_false 12 | end 13 | 14 | Then /^there should be a file called '(.*?)' somewhere in a subdirectory of '(.*?)'$/ do |file, directory| 15 | Dir.glob(File.join(file_path(directory), '**', file)).any?.should be_true 16 | end 17 | 18 | ### 19 | # IDENTICAL 20 | 21 | Then /^the file at '(.*?)' should be identical to the file at '(.*?)'$/ do |one, two| 22 | File.read(file_path(one)).should == File.read(file_path(two)) 23 | end 24 | 25 | Then /^the file at '(.*?)' should not be identical to the file at '(.*?)'$/ do |one, two| 26 | File.read(file_path(one)).should_not == File.read(file_path(two)) 27 | end 28 | 29 | Then /^the file called '(.*?)' in a subdirectory of '(.*?)' should be identical to the file at '(.*?)'$/ do |file, directory, other| 30 | File.read(Dir.glob(File.join(file_path(directory), '**', file)).first).should == File.read(file_path(other)) 31 | end 32 | 33 | Then /^the file called '(.*?)' in a subdirectory of '(.*?)' should not be identical to the file at '(.*?)'$/ do |file, directory, other| 34 | File.read(Dir.glob(File.join(file_path(directory), '**', file)).first).should_not == File.read(file_path(other)) 35 | end 36 | 37 | ### 38 | # CONTENT 39 | 40 | Then /^the file called '([^']+)' in a subdirectory of '([^']+)' should contain '([^']+)'$/ do |file, directory, content| 41 | File.read(Dir.glob(File.join(file_path(directory), '**', file)).first).should include(content) 42 | end 43 | 44 | Then /^the file at '([^']+)' should contain '([^']+)'$/ do |path, content| 45 | File.read(file_path(path)).should include(content) 46 | end 47 | 48 | ### 49 | # REVERSING 50 | 51 | Then /^the file at '(.*?)' should be the reverse of the file at '(.*?)'$/ do |one, two| 52 | File.read(file_path(one)).should == File.read(file_path(two)).reverse 53 | end 54 | -------------------------------------------------------------------------------- /features/file_storage.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And an instance of that class 9 | 10 | Scenario: store a file 11 | When I store the file 'fixtures/bork.txt' 12 | Then there should be a file at 'public/uploads/bork.txt' 13 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 14 | 15 | Scenario: store two files in succession 16 | When I store the file 'fixtures/bork.txt' 17 | Then there should be a file at 'public/uploads/bork.txt' 18 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 19 | When I store the file 'fixtures/monkey.txt' 20 | Then there should be a file at 'public/uploads/monkey.txt' 21 | And the file at 'public/uploads/monkey.txt' should be identical to the file at 'fixtures/monkey.txt' 22 | 23 | Scenario: cache a file and then store it 24 | When I cache the file 'fixtures/bork.txt' 25 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 26 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 27 | And there should not be a file at 'public/uploads/bork.txt' 28 | When I store the file 29 | Then there should be a file at 'public/uploads/bork.txt' 30 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 31 | 32 | Scenario: retrieving a file from cache then storing 33 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 34 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 35 | And I store the file 36 | Then there should be a file at 'public/uploads/bork.txt' 37 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 38 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/download.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'open-uri' 4 | 5 | module CarrierWave 6 | module Uploader 7 | module Download 8 | extend ActiveSupport::Concern 9 | 10 | include CarrierWave::Uploader::Callbacks 11 | include CarrierWave::Uploader::Configuration 12 | include CarrierWave::Uploader::Cache 13 | 14 | class RemoteFile 15 | def initialize(uri) 16 | @uri = uri 17 | end 18 | 19 | def original_filename 20 | File.basename(file.base_uri.path) 21 | end 22 | 23 | def respond_to?(*args) 24 | super or file.respond_to?(*args) 25 | end 26 | 27 | def http? 28 | @uri.scheme =~ /^https?$/ 29 | end 30 | 31 | private 32 | 33 | def file 34 | if @file.blank? 35 | @file = Kernel.open(@uri.to_s) 36 | @file = @file.is_a?(String) ? StringIO.new(@file) : @file 37 | end 38 | @file 39 | 40 | rescue 41 | raise CarrierWave::DownloadError, "could not download file" 42 | end 43 | 44 | def method_missing(*args, &block) 45 | file.send(*args, &block) 46 | end 47 | end 48 | 49 | ## 50 | # Caches the file by downloading it from the given URL. 51 | # 52 | # === Parameters 53 | # 54 | # [url (String)] The URL where the remote file is stored 55 | # 56 | def download!(uri) 57 | unless uri.blank? 58 | processed_uri = process_uri(uri) 59 | file = RemoteFile.new(processed_uri) 60 | raise CarrierWave::DownloadError, "trying to download a file which is not served over HTTP" unless file.http? 61 | cache!(file) 62 | end 63 | end 64 | 65 | ## 66 | # Processes the given URL by parsing and escaping it. Public to allow overriding. 67 | # 68 | # === Parameters 69 | # 70 | # [url (String)] The URL where the remote file is stored 71 | # 72 | def process_uri(uri) 73 | URI.parse(URI.escape(URI.unescape(uri))) 74 | end 75 | 76 | end # Download 77 | end # Uploader 78 | end # CarrierWave 79 | -------------------------------------------------------------------------------- /lib/carrierwave.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'fileutils' 4 | require 'active_support/core_ext/object/blank' 5 | require 'active_support/core_ext/class/attribute' 6 | require 'active_support/concern' 7 | 8 | module CarrierWave 9 | 10 | class << self 11 | attr_accessor :root, :base_path 12 | 13 | def configure(&block) 14 | CarrierWave::Uploader::Base.configure(&block) 15 | end 16 | 17 | def clean_cached_files!(seconds=60*60*24) 18 | CarrierWave::Uploader::Base.clean_cached_files!(seconds) 19 | end 20 | end 21 | 22 | end 23 | 24 | if defined?(Merb) 25 | 26 | CarrierWave.root = Merb.dir_for(:public) 27 | Merb::BootLoader.before_app_loads do 28 | # Setup path for uploaders and load all of them before classes are loaded 29 | Merb.push_path(:uploaders, Merb.root / 'app' / 'uploaders', '*.rb') 30 | Dir.glob(File.join(Merb.load_paths[:uploaders])).each {|f| require f } 31 | end 32 | 33 | elsif defined?(Rails) 34 | 35 | module CarrierWave 36 | class Railtie < Rails::Railtie 37 | initializer "carrierwave.setup_paths" do 38 | CarrierWave.root = Rails.root.join(Rails.public_path).to_s 39 | CarrierWave.base_path = ENV['RAILS_RELATIVE_URL_ROOT'] 40 | end 41 | 42 | initializer "carrierwave.active_record" do 43 | ActiveSupport.on_load :active_record do 44 | require 'carrierwave/orm/activerecord' 45 | end 46 | end 47 | end 48 | end 49 | 50 | elsif defined?(Sinatra) 51 | if defined?(Padrino) && defined?(PADRINO_ROOT) 52 | CarrierWave.root = File.join(PADRINO_ROOT, "public") 53 | else 54 | 55 | CarrierWave.root = if Sinatra::Application.respond_to?(:public_folder) 56 | # Sinatra >= 1.3 57 | Sinatra::Application.public_folder 58 | else 59 | # Sinatra < 1.3 60 | Sinatra::Application.public 61 | end 62 | end 63 | end 64 | 65 | require "carrierwave/error" 66 | require "carrierwave/sanitized_file" 67 | require "carrierwave/mount" 68 | require "carrierwave/processing" 69 | require "carrierwave/version" 70 | require "carrierwave/storage" 71 | require "carrierwave/uploader" 72 | require "carrierwave/compatibility/paperclip" 73 | require "carrierwave/test/matchers" 74 | -------------------------------------------------------------------------------- /features/file_storage_overridden_filename.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and overriden filename 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem with an overriden filename 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And that the uploader reverses the filename 9 | And an instance of that class 10 | 11 | Scenario: store a file 12 | When I store the file 'fixtures/bork.txt' 13 | Then there should be a file at 'public/uploads/txt.krob' 14 | And the file at 'public/uploads/txt.krob' should be identical to the file at 'fixtures/bork.txt' 15 | 16 | Scenario: store two files in succession 17 | When I store the file 'fixtures/bork.txt' 18 | Then there should be a file at 'public/uploads/txt.krob' 19 | And the file at 'public/uploads/txt.krob' should be identical to the file at 'fixtures/bork.txt' 20 | When I store the file 'fixtures/monkey.txt' 21 | Then there should be a file at 'public/uploads/txt.yeknom' 22 | And the file at 'public/uploads/txt.yeknom' should be identical to the file at 'fixtures/monkey.txt' 23 | 24 | Scenario: cache a file and then store it 25 | When I cache the file 'fixtures/bork.txt' 26 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 27 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 28 | And there should not be a file at 'public/uploads/txt.krob' 29 | When I store the file 30 | Then there should be a file at 'public/uploads/txt.krob' 31 | And the file at 'public/uploads/txt.krob' should be identical to the file at 'fixtures/bork.txt' 32 | 33 | Scenario: retrieving a file from cache then storing 34 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 35 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 36 | And I store the file 37 | Then there should be a file at 'public/uploads/txt.krob' 38 | And the file at 'public/uploads/txt.krob' should be identical to the file at 'fixtures/bork.txt' 39 | -------------------------------------------------------------------------------- /features/file_storage_overridden_store_dir.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and overridden store dir 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And that the uploader has the store_dir overridden to 'public/monkey/llama' 9 | And an instance of that class 10 | 11 | Scenario: store a file 12 | When I store the file 'fixtures/bork.txt' 13 | Then there should be a file at 'public/monkey/llama/bork.txt' 14 | And the file at 'public/monkey/llama/bork.txt' should be identical to the file at 'fixtures/bork.txt' 15 | 16 | Scenario: store two files in succession 17 | When I store the file 'fixtures/bork.txt' 18 | Then there should be a file at 'public/monkey/llama/bork.txt' 19 | And the file at 'public/monkey/llama/bork.txt' should be identical to the file at 'fixtures/bork.txt' 20 | When I store the file 'fixtures/monkey.txt' 21 | Then there should be a file at 'public/monkey/llama/monkey.txt' 22 | And the file at 'public/monkey/llama/monkey.txt' should be identical to the file at 'fixtures/monkey.txt' 23 | 24 | Scenario: cache a file and then store it 25 | When I cache the file 'fixtures/bork.txt' 26 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 27 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 28 | And there should not be a file at 'public/monkey/llama/bork.txt' 29 | When I store the file 30 | Then there should be a file at 'public/monkey/llama/bork.txt' 31 | And the file at 'public/monkey/llama/bork.txt' should be identical to the file at 'fixtures/bork.txt' 32 | 33 | Scenario: retrieving a file from cache then storing 34 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 35 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 36 | And I store the file 37 | Then there should be a file at 'public/monkey/llama/bork.txt' 38 | And the file at 'public/monkey/llama/bork.txt' should be identical to the file at 'fixtures/bork.txt' 39 | -------------------------------------------------------------------------------- /lib/carrierwave/processing/mime_types.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | 5 | ## 6 | # This module simplifies the use of the mime-types gem to intelligently 7 | # guess and set the content-type of a file. If you want to use this, you'll 8 | # need to require this file: 9 | # 10 | # require 'carrierwave/processing/mime_types' 11 | # 12 | # And then include it in your uploader: 13 | # 14 | # class MyUploader < CarrierWave::Uploader::Base 15 | # include CarrierWave::MimeTypes 16 | # end 17 | # 18 | # You can now use the provided helper: 19 | # 20 | # class MyUploader < CarrierWave::Uploader::Base 21 | # include CarrierWave::MimeTypes 22 | # 23 | # process :set_content_type 24 | # end 25 | # 26 | module MimeTypes 27 | extend ActiveSupport::Concern 28 | 29 | included do 30 | begin 31 | require "mime/types" 32 | rescue LoadError => e 33 | e.message << " (You may need to install the mime-types gem)" 34 | raise e 35 | end 36 | end 37 | 38 | module ClassMethods 39 | def set_content_type(override=false) 40 | process :set_content_type => override 41 | end 42 | end 43 | 44 | GENERIC_CONTENT_TYPES = %w[application/octet-stream binary/octet-stream] 45 | 46 | def generic_content_type? 47 | GENERIC_CONTENT_TYPES.include? file.content_type 48 | end 49 | 50 | ## 51 | # Changes the file content_type using the mime-types gem 52 | # 53 | # === Parameters 54 | # 55 | # [override (Boolean)] whether or not to override the file's content_type 56 | # if it is already set and not a generic content-type, 57 | # false by default 58 | # 59 | def set_content_type(override=false) 60 | if override || file.content_type.blank? || generic_content_type? 61 | new_content_type = ::MIME::Types.type_for(file.original_filename).first.to_s 62 | if file.respond_to?(:content_type=) 63 | file.content_type = new_content_type 64 | else 65 | file.instance_variable_set(:@content_type, new_content_type) 66 | end 67 | end 68 | rescue ::MIME::InvalidContentType => e 69 | raise CarrierWave::ProcessingError, I18n.translate(:"errors.messages.mime_types_processing_error", :e => e) 70 | end 71 | 72 | end # MimeTypes 73 | end # CarrierWave 74 | -------------------------------------------------------------------------------- /features/versions_caching_from_versions.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and versions with overridden store dir 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | Given a processor method named :upcase 9 | And that the uploader class has a version named 'thumb' which process 'upcase' 10 | And that the version 'thumb' has the store_dir overridden to 'public/monkey/llama' 11 | And that the uploader class has a version named 'small_thumb' which is based on version 'thumb' 12 | And that the version 'small_thumb' has the store_dir overridden to 'public/monkey/toro' 13 | And an instance of that class 14 | 15 | Scenario: cache a file and then store it 16 | When I cache the file 'fixtures/bork.txt' 17 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 18 | Then there should be a file called 'thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 19 | Then there should be a file called 'small_thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 20 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 21 | And the file called 'thumb_bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/upcased_bork.txt' 22 | And the file called 'small_thumb_bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/upcased_bork.txt' 23 | And there should not be a file at 'public/uploads/bork.txt' 24 | And there should not be a file at 'public/monkey/llama/thumb_bork.txt' 25 | And there should not be a file at 'public/monkey/toro/small_thumb_bork.txt' 26 | When I store the file 27 | Then there should be a file at 'public/uploads/bork.txt' 28 | Then there should be a file at 'public/monkey/llama/thumb_bork.txt' 29 | Then there should be a file at 'public/monkey/toro/small_thumb_bork.txt' 30 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 31 | And the file at 'public/monkey/llama/thumb_bork.txt' should be identical to the file at 'fixtures/upcased_bork.txt' 32 | And the file at 'public/monkey/toro/small_thumb_bork.txt' should be identical to the file at 'fixtures/upcased_bork.txt' -------------------------------------------------------------------------------- /lib/carrierwave/uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "carrierwave/uploader/configuration" 4 | require "carrierwave/uploader/callbacks" 5 | require "carrierwave/uploader/proxy" 6 | require "carrierwave/uploader/url" 7 | require "carrierwave/uploader/mountable" 8 | require "carrierwave/uploader/cache" 9 | require "carrierwave/uploader/store" 10 | require "carrierwave/uploader/download" 11 | require "carrierwave/uploader/remove" 12 | require "carrierwave/uploader/extension_whitelist" 13 | require "carrierwave/uploader/extension_blacklist" 14 | require "carrierwave/uploader/processing" 15 | require "carrierwave/uploader/versions" 16 | require "carrierwave/uploader/default_url" 17 | 18 | require "carrierwave/uploader/serialization" 19 | 20 | module CarrierWave 21 | 22 | ## 23 | # See CarrierWave::Uploader::Base 24 | # 25 | module Uploader 26 | 27 | ## 28 | # An uploader is a class that allows you to easily handle the caching and storage of 29 | # uploaded files. Please refer to the README for configuration options. 30 | # 31 | # Once you have an uploader you can use it in isolation: 32 | # 33 | # my_uploader = MyUploader.new 34 | # my_uploader.cache!(File.open(path_to_file)) 35 | # my_uploader.retrieve_from_store!('monkey.png') 36 | # 37 | # Alternatively, you can mount it on an ORM or other persistence layer, with 38 | # +CarrierWave::Mount#mount_uploader+. There are extensions for activerecord and datamapper 39 | # these are *very* simple (they are only a dozen lines of code), so adding your own should 40 | # be trivial. 41 | # 42 | class Base 43 | attr_reader :file 44 | 45 | include CarrierWave::Uploader::Configuration 46 | include CarrierWave::Uploader::Callbacks 47 | include CarrierWave::Uploader::Proxy 48 | include CarrierWave::Uploader::Url 49 | include CarrierWave::Uploader::Mountable 50 | include CarrierWave::Uploader::Cache 51 | include CarrierWave::Uploader::Store 52 | include CarrierWave::Uploader::Download 53 | include CarrierWave::Uploader::Remove 54 | include CarrierWave::Uploader::ExtensionWhitelist 55 | include CarrierWave::Uploader::ExtensionBlacklist 56 | include CarrierWave::Uploader::Processing 57 | include CarrierWave::Uploader::Versions 58 | include CarrierWave::Uploader::DefaultUrl 59 | include CarrierWave::Uploader::Serialization 60 | end # Base 61 | 62 | end # Uploader 63 | end # CarrierWave 64 | -------------------------------------------------------------------------------- /spec/uploader/default_url_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe 'with a default url' do 17 | before do 18 | @uploader_class.class_eval do 19 | version :thumb 20 | def default_url 21 | ["http://someurl.example.com", version_name].compact.join('/') 22 | end 23 | end 24 | @uploader = @uploader_class.new 25 | end 26 | 27 | describe '#blank?' do 28 | it "should be true by default" do 29 | @uploader.should be_blank 30 | end 31 | end 32 | 33 | describe '#current_path' do 34 | it "should return nil" do 35 | @uploader.current_path.should be_nil 36 | end 37 | end 38 | 39 | describe '#url' do 40 | it "should return the default url" do 41 | @uploader.url.should == 'http://someurl.example.com' 42 | end 43 | 44 | it "should return the default url with version when given" do 45 | @uploader.url(:thumb).should == 'http://someurl.example.com/thumb' 46 | end 47 | end 48 | 49 | describe '#cache!' do 50 | 51 | before do 52 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 53 | end 54 | 55 | it "should cache a file" do 56 | @uploader.cache!(File.open(file_path('test.jpg'))) 57 | @uploader.file.should be_an_instance_of(CarrierWave::SanitizedFile) 58 | end 59 | 60 | it "should be cached" do 61 | @uploader.cache!(File.open(file_path('test.jpg'))) 62 | @uploader.should be_cached 63 | end 64 | 65 | it "should no longer be blank" do 66 | @uploader.cache!(File.open(file_path('test.jpg'))) 67 | @uploader.should_not be_blank 68 | end 69 | 70 | it "should set the current_path" do 71 | @uploader.cache!(File.open(file_path('test.jpg'))) 72 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 73 | end 74 | 75 | it "should set the url" do 76 | @uploader.cache!(File.open(file_path('test.jpg'))) 77 | @uploader.url.should_not == 'http://someurl.example.com' 78 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 79 | end 80 | 81 | end 82 | 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /features/mount_activerecord.feature: -------------------------------------------------------------------------------- 1 | Feature: Mount an Uploader on ActiveRecord class 2 | In order to easily attach files to a form 3 | As a web developer using CarrierWave 4 | I want to mount an uploader on an ActiveRecord class 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And an activerecord class that uses the 'users' table 9 | And the uploader class is mounted on the 'avatar' column 10 | And an instance of the activerecord class 11 | 12 | Scenario: assign a file 13 | When I assign the file 'fixtures/bork.txt' to the 'avatar' column 14 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 15 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 16 | 17 | Scenario: assign a file and save the record 18 | When I assign the file 'fixtures/bork.txt' to the 'avatar' column 19 | And I save the active record 20 | Then there should be a file at 'public/uploads/bork.txt' 21 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 22 | And the url for the column 'avatar' should be '/uploads/bork.txt' 23 | 24 | Scenario: assign a file and retrieve it from cache 25 | When I assign the file 'fixtures/bork.txt' to the 'avatar' column 26 | And I retrieve the file later from the cache name for the column 'avatar' 27 | And I save the active record 28 | Then there should be a file at 'public/uploads/bork.txt' 29 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 30 | And the url for the column 'avatar' should be '/uploads/bork.txt' 31 | 32 | Scenario: store a file and retrieve it later 33 | When I assign the file 'fixtures/bork.txt' to the 'avatar' column 34 | And I retrieve the file later from the cache name for the column 'avatar' 35 | And I save the active record 36 | Then there should be a file at 'public/uploads/bork.txt' 37 | When I reload the active record 38 | Then the url for the column 'avatar' should be '/uploads/bork.txt' 39 | 40 | Scenario: store a file and delete the record 41 | When I assign the file 'fixtures/bork.txt' to the 'avatar' column 42 | And I retrieve the file later from the cache name for the column 'avatar' 43 | And I save the active record 44 | Then there should be a file at 'public/uploads/bork.txt' 45 | When I delete the active record 46 | Then there should not be a file at 'public/uploads/bork.txt' 47 | -------------------------------------------------------------------------------- /spec/processing/mime_types_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::MimeTypes do 6 | 7 | before do 8 | @klass = Class.new do 9 | attr_accessor :content_type 10 | include CarrierWave::MimeTypes 11 | end 12 | @instance = @klass.new 13 | FileUtils.cp(file_path('landscape.jpg'), file_path('landscape_copy.jpg')) 14 | @instance.stub(:original_filename).and_return file_path('landscape_copy.jpg') 15 | @instance.stub(:file).and_return CarrierWave::SanitizedFile.new(file_path('landscape_copy.jpg')) 16 | @file = @instance.file 17 | end 18 | 19 | after do 20 | FileUtils.rm(file_path('landscape_copy.jpg')) 21 | end 22 | 23 | describe '#set_content_type' do 24 | 25 | it "does not set content_type if already set" do 26 | @instance.file.content_type = 'image/jpeg' 27 | @instance.file.should_not_receive(:content_type=) 28 | @instance.set_content_type 29 | end 30 | 31 | it "set content_type if content_type is nil" do 32 | @instance.file.content_type = nil 33 | @instance.file.should_receive(:content_type=).with('image/jpeg') 34 | @instance.set_content_type 35 | end 36 | 37 | it "set content_type if content_type is empty" do 38 | @instance.file.content_type = '' 39 | @instance.file.should_receive(:content_type=).with('image/jpeg') 40 | @instance.set_content_type 41 | end 42 | 43 | %w[ application/octet-stream binary/octet-stream ].each do |type| 44 | it "sets content_type if content_type is generic (#{type})" do 45 | @instance.file.content_type = type 46 | @instance.file.should_receive(:content_type=).with('image/jpeg') 47 | @instance.set_content_type 48 | end 49 | end 50 | 51 | it "sets content_type if override is true" do 52 | @instance.file.content_type = 'image/jpeg' 53 | @instance.file.should_receive(:content_type=).with('image/jpeg') 54 | @instance.set_content_type(true) 55 | end 56 | 57 | end 58 | 59 | describe "test errors" do 60 | context "invalid mime type" do 61 | before do 62 | @instance.file.content_type = nil 63 | # TODO: somehow force a ::MIME::InvalidContentType error when set_content_type is called. 64 | end 65 | 66 | it "should raise a MIME::InvalidContentType error" do 67 | # lambda {@instance.set_content_type}.should raise_exception(::MIME::InvalidContentType, /^Failed to process file with MIME::Types, maybe not valid content-type\? Original Error:/) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/carrierwave/orm/activerecord.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'active_record' 4 | require 'carrierwave/validations/active_model' 5 | 6 | module CarrierWave 7 | module ActiveRecord 8 | 9 | include CarrierWave::Mount 10 | 11 | ## 12 | # See +CarrierWave::Mount#mount_uploader+ for documentation 13 | # 14 | def mount_uploader(column, uploader=nil, options={}, &block) 15 | super 16 | 17 | alias_method :read_uploader, :read_attribute 18 | alias_method :write_uploader, :write_attribute 19 | public :read_uploader 20 | public :write_uploader 21 | 22 | include CarrierWave::Validations::ActiveModel 23 | 24 | validates_integrity_of column if uploader_option(column.to_sym, :validate_integrity) 25 | validates_processing_of column if uploader_option(column.to_sym, :validate_processing) 26 | validates_download_of column if uploader_option(column.to_sym, :validate_download) 27 | 28 | after_save :"store_#{column}!" 29 | before_save :"write_#{column}_identifier" 30 | after_commit :"remove_#{column}!", :on => :destroy 31 | before_update :"store_previous_model_for_#{column}" 32 | after_save :"remove_previously_stored_#{column}" 33 | 34 | class_eval <<-RUBY, __FILE__, __LINE__+1 35 | def #{column}=(new_file) 36 | column = _mounter(:#{column}).serialization_column 37 | send(:"\#{column}_will_change!") 38 | super 39 | end 40 | 41 | def remote_#{column}_url=(url) 42 | column = _mounter(:#{column}).serialization_column 43 | send(:"\#{column}_will_change!") 44 | super 45 | end 46 | 47 | def remove_#{column}! 48 | super 49 | _mounter(:#{column}).remove = true 50 | _mounter(:#{column}).write_identifier 51 | end 52 | 53 | def serializable_hash(options=nil) 54 | hash = {} 55 | 56 | except = options && options[:except] && Array.wrap(options[:except]).map(&:to_s) 57 | only = options && options[:only] && Array.wrap(options[:only]).map(&:to_s) 58 | 59 | self.class.uploaders.each do |column, uploader| 60 | if (!only && !except) || (only && only.include?(column.to_s)) || (except && !except.include?(column.to_s)) 61 | hash[column.to_s] = _mounter(column).uploader.serializable_hash 62 | end 63 | end 64 | super(options).merge(hash) 65 | end 66 | RUBY 67 | 68 | end 69 | 70 | end # ActiveRecord 71 | end # CarrierWave 72 | 73 | ActiveRecord::Base.extend CarrierWave::ActiveRecord 74 | -------------------------------------------------------------------------------- /features/file_storage_reversing_processor.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and a processor that reverses the file 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And an instance of that class 9 | And the class has a method called 'reverse' that reverses the contents of a file 10 | And the class will process 'reverse' 11 | 12 | Scenario: store a file 13 | When I store the file 'fixtures/bork.txt' 14 | Then there should be a file at 'public/uploads/bork.txt' 15 | And the file at 'public/uploads/bork.txt' should not be identical to the file at 'fixtures/bork.txt' 16 | And the file at 'public/uploads/bork.txt' should be the reverse of the file at 'fixtures/bork.txt' 17 | 18 | Scenario: store two files in succession 19 | When I store the file 'fixtures/bork.txt' 20 | Then there should be a file at 'public/uploads/bork.txt' 21 | And the file at 'public/uploads/bork.txt' should not be identical to the file at 'fixtures/bork.txt' 22 | And the file at 'public/uploads/bork.txt' should be the reverse of the file at 'fixtures/bork.txt' 23 | When I store the file 'fixtures/monkey.txt' 24 | Then there should be a file at 'public/uploads/monkey.txt' 25 | And the file at 'public/uploads/monkey.txt' should not be identical to the file at 'fixtures/monkey.txt' 26 | And the file at 'public/uploads/monkey.txt' should be the reverse of the file at 'fixtures/monkey.txt' 27 | 28 | Scenario: cache a file and then store it 29 | When I cache the file 'fixtures/bork.txt' 30 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 31 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should not be identical to the file at 'fixtures/bork.txt' 32 | And there should not be a file at 'public/uploads/bork.txt' 33 | When I store the file 34 | Then there should be a file at 'public/uploads/bork.txt' 35 | And the file at 'public/uploads/bork.txt' should not be identical to the file at 'fixtures/bork.txt' 36 | And the file at 'public/uploads/bork.txt' should be the reverse of the file at 'fixtures/bork.txt' 37 | 38 | Scenario: retrieving a file from cache then storing 39 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 40 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 41 | And I store the file 42 | Then there should be a file at 'public/uploads/bork.txt' 43 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 44 | -------------------------------------------------------------------------------- /features/versions_overriden_store_dir.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and versions with overridden store dir 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And that the uploader class has a version named 'thumb' 9 | And that the version 'thumb' has the store_dir overridden to 'public/monkey/llama' 10 | And an instance of that class 11 | 12 | Scenario: store a file 13 | When I store the file 'fixtures/bork.txt' 14 | Then there should be a file at 'public/uploads/bork.txt' 15 | Then there should be a file at 'public/monkey/llama/thumb_bork.txt' 16 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 17 | And the file at 'public/monkey/llama/thumb_bork.txt' should be identical to the file at 'fixtures/bork.txt' 18 | 19 | Scenario: cache a file and then store it 20 | When I cache the file 'fixtures/bork.txt' 21 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 22 | Then there should be a file called 'thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 23 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 24 | And the file called 'thumb_bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 25 | And there should not be a file at 'public/uploads/bork.txt' 26 | And there should not be a file at 'public/monkey/llama/thumb_bork.txt' 27 | When I store the file 28 | Then there should be a file at 'public/uploads/bork.txt' 29 | Then there should be a file at 'public/monkey/llama/thumb_bork.txt' 30 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 31 | And the file at 'public/monkey/llama/thumb_bork.txt' should be identical to the file at 'fixtures/bork.txt' 32 | 33 | Scenario: retrieving a file from cache then storing 34 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 35 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/thumb_bork.txt' 36 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 37 | And I store the file 38 | Then there should be a file at 'public/uploads/bork.txt' 39 | Then there should be a file at 'public/monkey/llama/thumb_bork.txt' 40 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 41 | And the file at 'public/monkey/llama/thumb_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 42 | -------------------------------------------------------------------------------- /spec/uploader/overrides_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader_class.configure do |config| 10 | 11 | config.fog_credentials = { 12 | :provider => 'AWS', # required 13 | :aws_access_key_id => 'XXXX', # required 14 | :aws_secret_access_key => 'YYYY', # required 15 | :region => 'us-east-1' # optional, defaults to 'us-east-1' 16 | } 17 | 18 | config.fog_directory = "defaultbucket" 19 | end 20 | 21 | @uploader = @uploader_class.new 22 | @uploader_overridden = @uploader_class.new 23 | @uploader_overridden.fog_credentials = { 24 | :provider => 'AWS', # required 25 | :aws_access_key_id => 'ZZZZ', # required 26 | :aws_secret_access_key => 'AAAA', # required 27 | :region => 'us-east-2' # optional, defaults to 'us-east-1' 28 | } 29 | @uploader_overridden.fog_public = false 30 | end 31 | 32 | describe 'fog_credentials' do 33 | it 'should reflect the standard value if no override done' do 34 | @uploader.fog_credentials.should be_a(Hash) 35 | @uploader.fog_credentials[:provider].should be_eql('AWS') 36 | @uploader.fog_credentials[:aws_access_key_id].should be_eql('XXXX') 37 | @uploader.fog_credentials[:aws_secret_access_key].should be_eql('YYYY') 38 | @uploader.fog_credentials[:region].should be_eql('us-east-1') 39 | end 40 | 41 | it 'should reflect the new values in uploader class with override' do 42 | @uploader_overridden.fog_credentials.should be_a(Hash) 43 | @uploader_overridden.fog_credentials[:provider].should be_eql('AWS') 44 | @uploader_overridden.fog_credentials[:aws_access_key_id].should be_eql('ZZZZ') 45 | @uploader_overridden.fog_credentials[:aws_secret_access_key].should be_eql('AAAA') 46 | @uploader_overridden.fog_credentials[:region].should be_eql('us-east-2') 47 | end 48 | end 49 | 50 | describe 'fog_directory' do 51 | it 'should reflect the standard value if no override done' do 52 | @uploader.fog_directory.should be_eql('defaultbucket') 53 | end 54 | 55 | it 'should reflect the standard value in overridden object because property is not overridden' do 56 | @uploader_overridden.fog_directory.should be_eql('defaultbucket') 57 | end 58 | end 59 | 60 | describe 'fog_public' do 61 | it 'should reflect the standard value if no override done' do 62 | @uploader.fog_public.should be_eql(true) 63 | end 64 | 65 | it 'should reflect the standard value in overridden object because property is not overridden' do 66 | @uploader_overridden.fog_public.should be_eql(false) 67 | end 68 | end 69 | end -------------------------------------------------------------------------------- /lib/carrierwave/validations/active_model.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'active_model/validator' 4 | require 'active_support/concern' 5 | 6 | module CarrierWave 7 | 8 | # == Active Model Presence Validator 9 | module Validations 10 | module ActiveModel 11 | extend ActiveSupport::Concern 12 | 13 | class ProcessingValidator < ::ActiveModel::EachValidator 14 | 15 | def validate_each(record, attribute, value) 16 | if e = record.send("#{attribute}_processing_error") 17 | message = (e.message == e.class.to_s) ? :carrierwave_processing_error : e.message 18 | record.errors.add(attribute, message) 19 | end 20 | end 21 | end 22 | 23 | class IntegrityValidator < ::ActiveModel::EachValidator 24 | 25 | def validate_each(record, attribute, value) 26 | if e = record.send("#{attribute}_integrity_error") 27 | message = (e.message == e.class.to_s) ? :carrierwave_integrity_error : e.message 28 | record.errors.add(attribute, message) 29 | end 30 | end 31 | end 32 | 33 | class DownloadValidator < ::ActiveModel::EachValidator 34 | 35 | def validate_each(record, attribute, value) 36 | if e = record.send("#{attribute}_download_error") 37 | message = (e.message == e.class.to_s) ? :carrierwave_download_error : e.message 38 | record.errors.add(attribute, message) 39 | end 40 | end 41 | end 42 | 43 | module HelperMethods 44 | 45 | ## 46 | # Makes the record invalid if the file couldn't be uploaded due to an integrity error 47 | # 48 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 49 | # 50 | def validates_integrity_of(*attr_names) 51 | validates_with IntegrityValidator, _merge_attributes(attr_names) 52 | end 53 | 54 | ## 55 | # Makes the record invalid if the file couldn't be processed (assuming the process failed 56 | # with a CarrierWave::ProcessingError) 57 | # 58 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 59 | # 60 | def validates_processing_of(*attr_names) 61 | validates_with ProcessingValidator, _merge_attributes(attr_names) 62 | end 63 | # 64 | ## 65 | # Makes the record invalid if the remote file couldn't be downloaded 66 | # 67 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 68 | # 69 | def validates_download_of(*attr_names) 70 | validates_with DownloadValidator, _merge_attributes(attr_names) 71 | end 72 | end 73 | 74 | included do 75 | extend HelperMethods 76 | include HelperMethods 77 | end 78 | end 79 | end 80 | end 81 | 82 | I18n.load_path << File.join(File.dirname(__FILE__), "..", "locale", 'en.yml') 83 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/processing.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Processing 6 | extend ActiveSupport::Concern 7 | 8 | include CarrierWave::Uploader::Callbacks 9 | 10 | included do 11 | class_attribute :processors, :instance_writer => false 12 | self.processors = [] 13 | 14 | after :cache, :process! 15 | end 16 | 17 | module ClassMethods 18 | 19 | ## 20 | # Adds a processor callback which applies operations as a file is uploaded. 21 | # The argument may be the name of any method of the uploader, expressed as a symbol, 22 | # or a list of such methods, or a hash where the key is a method and the value is 23 | # an array of arguments to call the method with 24 | # 25 | # === Parameters 26 | # 27 | # args (*Symbol, Hash{Symbol => Array[]}) 28 | # 29 | # === Examples 30 | # 31 | # class MyUploader < CarrierWave::Uploader::Base 32 | # 33 | # process :sepiatone, :vignette 34 | # process :scale => [200, 200] 35 | # process :scale => [200, 200], :if => :image? 36 | # process :sepiatone, :if => :image? 37 | # 38 | # def sepiatone 39 | # ... 40 | # end 41 | # 42 | # def vignette 43 | # ... 44 | # end 45 | # 46 | # def scale(height, width) 47 | # ... 48 | # end 49 | # 50 | # def image? 51 | # ... 52 | # end 53 | # 54 | # end 55 | # 56 | def process(*args) 57 | if !args.first.is_a?(Hash) && args.last.is_a?(Hash) 58 | conditions = args.pop 59 | args.map!{ |arg| {arg => []}.merge(conditions) } 60 | end 61 | 62 | args.each do |arg| 63 | if arg.is_a?(Hash) 64 | condition = arg.delete(:if) 65 | arg.each do |method, args| 66 | self.processors += [[method, args, condition]] 67 | end 68 | else 69 | self.processors += [[arg, [], nil]] 70 | end 71 | end 72 | end 73 | 74 | end # ClassMethods 75 | 76 | ## 77 | # Apply all process callbacks added through CarrierWave.process 78 | # 79 | def process!(new_file=nil) 80 | if enable_processing 81 | self.class.processors.each do |method, args, condition| 82 | if(condition) 83 | next if !(condition.respond_to?(:call) ? condition.call(self, :args => args, :method => method, :file => new_file) : self.send(condition, new_file)) 84 | end 85 | self.send(method, *args) 86 | end 87 | end 88 | end 89 | 90 | end # Processing 91 | end # Uploader 92 | end # CarrierWave 93 | -------------------------------------------------------------------------------- /spec/uploader/extension_blacklist_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | before do 7 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 8 | @uploader = @uploader_class.new 9 | end 10 | 11 | after do 12 | FileUtils.rm_rf(public_path) 13 | end 14 | 15 | describe '#cache!' do 16 | 17 | before do 18 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 19 | end 20 | 21 | it "should not raise an integrity error if there is no black list" do 22 | @uploader.stub!(:extension_black_list).and_return(nil) 23 | running { 24 | @uploader.cache!(File.open(file_path('test.jpg'))) 25 | }.should_not raise_error(CarrierWave::IntegrityError) 26 | end 27 | 28 | it "should raise an integrity error if there is a black list and the file is on it" do 29 | @uploader.stub!(:extension_black_list).and_return(%w(jpg gif png)) 30 | running { 31 | @uploader.cache!(File.open(file_path('test.jpg'))) 32 | }.should raise_error(CarrierWave::IntegrityError) 33 | end 34 | 35 | it "should not raise an integrity error if there is a black list and the file is not on it" do 36 | @uploader.stub!(:extension_black_list).and_return(%w(txt doc xls)) 37 | running { 38 | @uploader.cache!(File.open(file_path('test.jpg'))) 39 | }.should_not raise_error(CarrierWave::IntegrityError) 40 | end 41 | 42 | it "should not raise an integrity error if there is a black list and the file is not on it, using start of string matcher" do 43 | @uploader.stub!(:extension_black_list).and_return(%w(txt)) 44 | running { 45 | @uploader.cache!(File.open(file_path('bork.ttxt'))) 46 | }.should_not raise_error(CarrierWave::IntegrityError) 47 | end 48 | 49 | it "should not raise an integrity error if there is a black list and the file is not on it, using end of string matcher" do 50 | @uploader.stub!(:extension_black_list).and_return(%w(txt)) 51 | running { 52 | @uploader.cache!(File.open(file_path('bork.txtt'))) 53 | }.should_not raise_error(CarrierWave::IntegrityError) 54 | end 55 | 56 | it "should compare black list in a case insensitive manner when capitalized extension provided" do 57 | @uploader.stub!(:extension_black_list).and_return(%w(jpg gif png)) 58 | running { 59 | @uploader.cache!(File.open(file_path('case.JPG'))) 60 | }.should raise_error(CarrierWave::IntegrityError) 61 | end 62 | 63 | it "should compare black list in a case insensitive manner when lowercase extension provided" do 64 | @uploader.stub!(:extension_black_list).and_return(%w(JPG GIF PNG)) 65 | running { 66 | @uploader.cache!(File.open(file_path('test.jpg'))) 67 | }.should raise_error(CarrierWave::IntegrityError) 68 | end 69 | 70 | it "should accept and check regular expressions" do 71 | @uploader.stub!(:extension_black_list).and_return([/jpe?g/, 'gif', 'png']) 72 | running { 73 | @uploader.cache!(File.open(file_path('test.jpeg'))) 74 | }.should raise_error(CarrierWave::IntegrityError) 75 | end 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /spec/uploader/extension_whitelist_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe '#cache!' do 17 | 18 | before do 19 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 20 | end 21 | 22 | it "should not raise an integrity error if there is no white list" do 23 | @uploader.stub!(:extension_white_list).and_return(nil) 24 | running { 25 | @uploader.cache!(File.open(file_path('test.jpg'))) 26 | }.should_not raise_error(CarrierWave::IntegrityError) 27 | end 28 | 29 | it "should not raise an integrity error if there is a white list and the file is on it" do 30 | @uploader.stub!(:extension_white_list).and_return(%w(jpg gif png)) 31 | running { 32 | @uploader.cache!(File.open(file_path('test.jpg'))) 33 | }.should_not raise_error(CarrierWave::IntegrityError) 34 | end 35 | 36 | it "should raise an integrity error if there is a white list and the file is not on it" do 37 | @uploader.stub!(:extension_white_list).and_return(%w(txt doc xls)) 38 | running { 39 | @uploader.cache!(File.open(file_path('test.jpg'))) 40 | }.should raise_error(CarrierWave::IntegrityError) 41 | end 42 | 43 | it "should raise an integrity error if there is a white list and the file is not on it, using start of string matcher" do 44 | @uploader.stub!(:extension_white_list).and_return(%w(txt)) 45 | running { 46 | @uploader.cache!(File.open(file_path('bork.ttxt'))) 47 | }.should raise_error(CarrierWave::IntegrityError) 48 | end 49 | 50 | it "should raise an integrity error if there is a white list and the file is not on it, using end of string matcher" do 51 | @uploader.stub!(:extension_white_list).and_return(%w(txt)) 52 | running { 53 | @uploader.cache!(File.open(file_path('bork.txtt'))) 54 | }.should raise_error(CarrierWave::IntegrityError) 55 | end 56 | 57 | it "should compare white list in a case insensitive manner when capitalized extension provided" do 58 | @uploader.stub!(:extension_white_list).and_return(%w(jpg gif png)) 59 | running { 60 | @uploader.cache!(File.open(file_path('case.JPG'))) 61 | }.should_not raise_error(CarrierWave::IntegrityError) 62 | end 63 | 64 | it "should compare white list in a case insensitive manner when lowercase extension provided" do 65 | @uploader.stub!(:extension_white_list).and_return(%w(JPG GIF PNG)) 66 | running { 67 | @uploader.cache!(File.open(file_path('test.jpg'))) 68 | }.should_not raise_error(CarrierWave::IntegrityError) 69 | end 70 | 71 | it "should accept and check regular expressions" do 72 | @uploader.stub!(:extension_white_list).and_return([/jpe?g/, 'gif', 'png']) 73 | running { 74 | @uploader.cache!(File.open(file_path('test.jpeg'))) 75 | }.should_not raise_error(CarrierWave::IntegrityError) 76 | end 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /features/versions_basics.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and versions 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And that the uploader class has a version named 'thumb' 9 | And an instance of that class 10 | 11 | Scenario: store a file 12 | When I store the file 'fixtures/bork.txt' 13 | Then there should be a file at 'public/uploads/bork.txt' 14 | Then there should be a file at 'public/uploads/thumb_bork.txt' 15 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 16 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/bork.txt' 17 | And the uploader should have the url '/uploads/bork.txt' 18 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 19 | 20 | Scenario: cache a file and then store it 21 | When I cache the file 'fixtures/bork.txt' 22 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 23 | Then there should be a file called 'thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 24 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 25 | And there should not be a file at 'public/uploads/bork.txt' 26 | And there should not be a file at 'public/uploads/thumb_bork.txt' 27 | When I store the file 28 | Then there should be a file at 'public/uploads/bork.txt' 29 | And there should be a file at 'public/uploads/thumb_bork.txt' 30 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 31 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/bork.txt' 32 | And the uploader should have the url '/uploads/bork.txt' 33 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 34 | 35 | Scenario: retrieving a file from cache then storing 36 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 37 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/thumb_bork.txt' 38 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 39 | And I store the file 40 | Then there should be a file at 'public/uploads/bork.txt' 41 | Then there should be a file at 'public/uploads/thumb_bork.txt' 42 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 43 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 44 | 45 | Scenario: retrieving a file from store 46 | Given the file 'fixtures/bork.txt' is stored at 'public/uploads/bork.txt' 47 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_bork.txt' 48 | When I retrieve the file 'bork.txt' from the store 49 | Then the uploader should have the url '/uploads/bork.txt' 50 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 51 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | 6 | require 'tempfile' 7 | require 'time' 8 | require 'logger' 9 | 10 | require 'carrierwave' 11 | require 'timecop' 12 | require 'open-uri' 13 | require 'sham_rack' 14 | 15 | require 'mysql2' 16 | 17 | require 'fog' 18 | require 'storage/fog_helper' 19 | 20 | unless ENV['REMOTE'] == 'true' 21 | Fog.mock! 22 | end 23 | 24 | require 'fog_credentials' # after Fog.mock! 25 | 26 | CARRIERWAVE_DIRECTORY = "carrierwave#{Time.now.to_i}" unless defined?(CARRIERWAVE_DIRECTORY) 27 | 28 | alias :running :lambda 29 | 30 | def file_path( *paths ) 31 | File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', *paths)) 32 | end 33 | 34 | def public_path( *paths ) 35 | File.expand_path(File.join(File.dirname(__FILE__), 'public', *paths)) 36 | end 37 | 38 | CarrierWave.root = public_path 39 | 40 | module CarrierWave 41 | module Test 42 | module MockStorage 43 | def mock_storage(kind) 44 | storage = mock("storage for #{kind} uploader") 45 | storage.stub!(:setup!) 46 | storage 47 | end 48 | end 49 | 50 | module MockFiles 51 | def stub_merb_tempfile(filename) 52 | raise "#{path} file does not exist" unless File.exist?(file_path(filename)) 53 | 54 | t = Tempfile.new(filename) 55 | FileUtils.copy_file(file_path(filename), t.path) 56 | 57 | return t 58 | end 59 | 60 | def stub_tempfile(filename, mime_type=nil, fake_name=nil) 61 | raise "#{path} file does not exist" unless File.exist?(file_path(filename)) 62 | 63 | t = Tempfile.new(filename) 64 | FileUtils.copy_file(file_path(filename), t.path) 65 | 66 | # This is stupid, but for some reason rspec won't play nice... 67 | eval <<-EOF 68 | def t.original_filename; '#{fake_name || filename}'; end 69 | def t.content_type; '#{mime_type}'; end 70 | def t.local_path; path; end 71 | EOF 72 | 73 | return t 74 | end 75 | 76 | def stub_stringio(filename, mime_type=nil, fake_name=nil) 77 | if filename 78 | t = StringIO.new( IO.read( file_path( filename ) ) ) 79 | else 80 | t = StringIO.new 81 | end 82 | t.stub!(:local_path => "", 83 | :original_filename => filename || fake_name, 84 | :content_type => mime_type) 85 | return t 86 | end 87 | 88 | def stub_file(filename, mime_type=nil, fake_name=nil) 89 | f = File.open(file_path(filename)) 90 | return f 91 | end 92 | end 93 | 94 | module I18nHelpers 95 | def change_locale_and_store_translations(locale, translations, &block) 96 | current_locale = I18n.locale 97 | begin 98 | I18n.backend.store_translations locale, translations 99 | I18n.locale = locale 100 | yield 101 | ensure 102 | I18n.reload! 103 | I18n.locale = current_locale 104 | end 105 | end 106 | end 107 | end 108 | end 109 | 110 | RSpec.configure do |config| 111 | config.include CarrierWave::Test::Matchers 112 | config.include CarrierWave::Test::MockFiles 113 | config.include CarrierWave::Test::MockStorage 114 | config.include CarrierWave::Test::I18nHelpers 115 | end 116 | -------------------------------------------------------------------------------- /features/step_definitions/general_steps.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Given /^an uploader class that uses the '(.*?)' storage$/ do |kind| 4 | @klass = Class.new(CarrierWave::Uploader::Base) 5 | @klass.storage = kind.to_sym 6 | end 7 | 8 | Given /^an instance of that class$/ do 9 | @uploader = @klass.new 10 | end 11 | 12 | Given /^a processor method named :upcase$/ do 13 | @klass.class_eval do 14 | define_method(:upcase) do 15 | content = File.read(current_path) 16 | File.open(current_path, 'w') { |f| f.write content.upcase } 17 | end 18 | end 19 | end 20 | 21 | Then /^the contents of the file should be '(.*?)'$/ do |contents| 22 | @uploader.read.chomp.should == contents 23 | end 24 | 25 | Given /^that the uploader reverses the filename$/ do 26 | @klass.class_eval do 27 | def filename 28 | super.reverse unless super.blank? 29 | end 30 | end 31 | end 32 | 33 | Given /^that the uploader has the filename overridden to '(.*?)'$/ do |filename| 34 | @klass.class_eval do 35 | define_method(:filename) do 36 | filename 37 | end 38 | end 39 | end 40 | 41 | Given /^that the uploader has the store_dir overridden to '(.*?)'$/ do |store_dir| 42 | @klass.class_eval do 43 | define_method(:store_dir) do 44 | file_path(store_dir) 45 | end 46 | end 47 | end 48 | 49 | Given /^that the version '(.*?)' has the store_dir overridden to '(.*?)'$/ do |version, store_dir| 50 | @klass.versions[version.to_sym][:uploader].class_eval do 51 | define_method(:store_dir) do 52 | file_path(store_dir) 53 | end 54 | end 55 | end 56 | 57 | Given /^that the uploader class has a version named '([^\']+)'$/ do |name| 58 | @klass.version(name) 59 | end 60 | 61 | Given /^that the uploader class has a version named '([^\']+)' which process '([a-zA-Z0-9\_\?!]*)'$/ do |name, processor_name| 62 | @klass.version(name) do 63 | process processor_name.to_sym 64 | end 65 | end 66 | 67 | Given /^that the uploader class has a version named '([^\']+)' which is based on version '(.*?)'$/ do |name, based_version_name| 68 | @klass.version(name, {:from_version => based_version_name.to_sym}) 69 | end 70 | 71 | Given /^yo dawg, I put a version called '(.*?)' in your version called '(.*?)'$/ do |v2, v1| 72 | @klass.version(v1) do 73 | version(v2) 74 | end 75 | end 76 | 77 | Given /^the class has a method called 'reverse' that reverses the contents of a file$/ do 78 | @klass.class_eval do 79 | def reverse 80 | text = File.read(current_path) 81 | File.open(current_path, 'w') { |f| f.write(text.reverse) } 82 | end 83 | end 84 | end 85 | 86 | Given /^the class will process '([a-zA-Z0-9\_\?!]*)'$/ do |name| 87 | @klass.process name.to_sym 88 | end 89 | 90 | Then /^the uploader should have '(.*?)' as its current path$/ do |path| 91 | @uploader.current_path.should == file_path(path) 92 | end 93 | 94 | Then /^the uploader should have the url '(.*?)'$/ do |url| 95 | @uploader.url.should == url 96 | end 97 | 98 | Then /^the uploader's version '(.*?)' should have the url '(.*?)'$/ do |version, url| 99 | @uploader.versions[version.to_sym].url.should == url 100 | end 101 | 102 | Then /^the uploader's nested version '(.*?)' nested in '(.*?)' should have the url '(.*?)'$/ do |v2, v1, url| 103 | @uploader.versions[v1.to_sym].versions[v2.to_sym].url.should == url 104 | end 105 | -------------------------------------------------------------------------------- /spec/uploader/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | 6 | describe CarrierWave do 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | end 10 | 11 | describe '.configure' do 12 | it "should proxy to Uploader configuration" do 13 | CarrierWave::Uploader::Base.add_config :test_config 14 | CarrierWave.configure do |config| 15 | config.test_config = "foo" 16 | end 17 | CarrierWave::Uploader::Base.test_config.should == 'foo' 18 | end 19 | end 20 | end 21 | 22 | describe CarrierWave::Uploader::Base do 23 | before do 24 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 25 | end 26 | 27 | describe '.configure' do 28 | it "should set a configuration parameter" do 29 | @uploader_class.add_config :foo_bar 30 | @uploader_class.configure do |config| 31 | config.foo_bar = "monkey" 32 | end 33 | @uploader_class.foo_bar.should == 'monkey' 34 | end 35 | end 36 | 37 | describe ".storage" do 38 | it "should set the storage if an argument is given" do 39 | storage = mock('some kind of storage') 40 | @uploader_class.storage storage 41 | @uploader_class.storage.should == storage 42 | end 43 | 44 | it "should default to file" do 45 | @uploader_class.storage.should == CarrierWave::Storage::File 46 | end 47 | 48 | it "should set the storage from the configured shortcuts if a symbol is given" do 49 | @uploader_class.storage :file 50 | @uploader_class.storage.should == CarrierWave::Storage::File 51 | end 52 | 53 | it "should remember the storage when inherited" do 54 | @uploader_class.storage :fog 55 | subclass = Class.new(@uploader_class) 56 | subclass.storage.should == CarrierWave::Storage::Fog 57 | end 58 | 59 | it "should be changeable when inherited" do 60 | @uploader_class.storage :fog 61 | subclass = Class.new(@uploader_class) 62 | subclass.storage.should == CarrierWave::Storage::Fog 63 | subclass.storage :file 64 | subclass.storage.should == CarrierWave::Storage::File 65 | end 66 | end 67 | 68 | 69 | describe '.add_config' do 70 | it "should add a class level accessor" do 71 | @uploader_class.add_config :foo_bar 72 | @uploader_class.foo_bar = 'foo' 73 | @uploader_class.foo_bar.should == 'foo' 74 | end 75 | 76 | ['foo', :foo, 45, ['foo', :bar]].each do |val| 77 | it "should be inheritable for a #{val.class}" do 78 | @uploader_class.add_config :foo_bar 79 | @child_class = Class.new(@uploader_class) 80 | 81 | @uploader_class.foo_bar = val 82 | @uploader_class.foo_bar.should == val 83 | @child_class.foo_bar.should == val 84 | 85 | @child_class.foo_bar = "bar" 86 | @child_class.foo_bar.should == "bar" 87 | 88 | @uploader_class.foo_bar.should == val 89 | end 90 | end 91 | 92 | 93 | it "should add an instance level accessor" do 94 | @uploader_class.add_config :foo_bar 95 | @uploader_class.foo_bar = 'foo' 96 | @uploader_class.new.foo_bar.should == 'foo' 97 | end 98 | 99 | it "should add a convenient in-class setter" do 100 | @uploader_class.add_config :foo_bar 101 | @uploader_class.foo_bar "monkey" 102 | @uploader_class.foo_bar.should == "monkey" 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /features/versions_overridden_filename.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and overriden filename 2 | In order to customize the filaname of uploaded files 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem with an overriden filename and different verions 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And that the uploader class has a version named 'thumb' 9 | And that the uploader has the filename overridden to 'grark.png' 10 | And an instance of that class 11 | 12 | Scenario: store a file 13 | When I store the file 'fixtures/bork.txt' 14 | Then there should be a file at 'public/uploads/grark.png' 15 | Then there should be a file at 'public/uploads/thumb_grark.png' 16 | And the file at 'public/uploads/grark.png' should be identical to the file at 'fixtures/bork.txt' 17 | And the file at 'public/uploads/thumb_grark.png' should be identical to the file at 'fixtures/bork.txt' 18 | And the uploader should have the url '/uploads/grark.png' 19 | And the uploader's version 'thumb' should have the url '/uploads/thumb_grark.png' 20 | 21 | Scenario: cache a file and then store it 22 | When I cache the file 'fixtures/bork.txt' 23 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 24 | Then there should be a file called 'thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 25 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 26 | And there should not be a file at 'public/uploads/grark.png' 27 | And there should not be a file at 'public/uploads/thumb_grark.png' 28 | When I store the file 29 | Then there should be a file at 'public/uploads/grark.png' 30 | And there should be a file at 'public/uploads/thumb_grark.png' 31 | And the file at 'public/uploads/grark.png' should be identical to the file at 'fixtures/bork.txt' 32 | And the file at 'public/uploads/thumb_grark.png' should be identical to the file at 'fixtures/bork.txt' 33 | And the uploader should have the url '/uploads/grark.png' 34 | And the uploader's version 'thumb' should have the url '/uploads/thumb_grark.png' 35 | 36 | Scenario: retrieving a file from cache then storing 37 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 38 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/thumb_bork.txt' 39 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 40 | And I store the file 41 | Then there should be a file at 'public/uploads/grark.png' 42 | Then there should be a file at 'public/uploads/thumb_grark.png' 43 | And the file at 'public/uploads/grark.png' should be identical to the file at 'fixtures/bork.txt' 44 | And the file at 'public/uploads/thumb_grark.png' should be identical to the file at 'fixtures/monkey.txt' 45 | 46 | Scenario: retrieving a file from store 47 | Given the file 'fixtures/bork.txt' is stored at 'public/uploads/bork.txt' 48 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_bork.txt' 49 | When I retrieve the file 'bork.txt' from the store 50 | Then the uploader should have the url '/uploads/bork.txt' 51 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 52 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/store.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Store 6 | extend ActiveSupport::Concern 7 | 8 | include CarrierWave::Uploader::Callbacks 9 | include CarrierWave::Uploader::Configuration 10 | include CarrierWave::Uploader::Cache 11 | 12 | ## 13 | # Override this in your Uploader to change the filename. 14 | # 15 | # Be careful using record ids as filenames. If the filename is stored in the database 16 | # the record id will be nil when the filename is set. Don't use record ids unless you 17 | # understand this limitation. 18 | # 19 | # Do not use the version_name in the filename, as it will prevent versions from being 20 | # loaded correctly. 21 | # 22 | # === Returns 23 | # 24 | # [String] a filename 25 | # 26 | def filename 27 | @filename 28 | end 29 | 30 | ## 31 | # Calculates the path where the file should be stored. If +for_file+ is given, it will be 32 | # used as the filename, otherwise +CarrierWave::Uploader#filename+ is assumed. 33 | # 34 | # === Parameters 35 | # 36 | # [for_file (String)] name of the file 37 | # 38 | # === Returns 39 | # 40 | # [String] the store path 41 | # 42 | def store_path(for_file=filename) 43 | File.join([store_dir, full_filename(for_file)].compact) 44 | end 45 | 46 | ## 47 | # Stores the file by passing it to this Uploader's storage engine. 48 | # 49 | # If new_file is omitted, a previously cached file will be stored. 50 | # 51 | # === Parameters 52 | # 53 | # [new_file (File, IOString, Tempfile)] any kind of file object 54 | # 55 | def store!(new_file=nil) 56 | cache!(new_file) if new_file && ((@cache_id != parent_cache_id) || @cache_id.nil?) 57 | if @file and @cache_id 58 | with_callbacks(:store, new_file) do 59 | new_file = storage.store!(@file) 60 | @file.delete if (delete_tmp_file_after_storage && ! move_to_store) 61 | delete_cache_id 62 | @file = new_file 63 | @cache_id = nil 64 | end 65 | end 66 | end 67 | 68 | ## 69 | # Deletes a cache id (tmp dir in cache) 70 | # 71 | def delete_cache_id 72 | if @cache_id 73 | path = File.expand_path(File.join(cache_dir, @cache_id), CarrierWave.root) 74 | begin 75 | Dir.rmdir(path) 76 | rescue Errno::ENOENT 77 | # Ignore: path does not exist 78 | rescue Errno::ENOTDIR 79 | # Ignore: path is not a dir 80 | rescue Errno::ENOTEMPTY, Errno::EEXIST 81 | # Ignore: dir is not empty 82 | end 83 | end 84 | end 85 | 86 | ## 87 | # Retrieves the file from the storage. 88 | # 89 | # === Parameters 90 | # 91 | # [identifier (String)] uniquely identifies the file to retrieve 92 | # 93 | def retrieve_from_store!(identifier) 94 | with_callbacks(:retrieve_from_store, identifier) do 95 | @file = storage.retrieve!(identifier) 96 | end 97 | end 98 | 99 | private 100 | 101 | def full_filename(for_file) 102 | for_file 103 | end 104 | 105 | def storage 106 | @storage ||= self.class.storage.new(self) 107 | end 108 | 109 | end # Store 110 | end # Uploader 111 | end # CarrierWave 112 | -------------------------------------------------------------------------------- /lib/carrierwave/compatibility/paperclip.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Compatibility 5 | 6 | ## 7 | # Mix this module into an Uploader to make it mimic Paperclip's storage paths 8 | # This will make your Uploader use the same default storage path as paperclip 9 | # does. If you need to override it, you can override the +paperclip_path+ method 10 | # and provide a Paperclip style path: 11 | # 12 | # class MyUploader < CarrierWave::Uploader::Base 13 | # include CarrierWave::Compatibility::Paperclip 14 | # 15 | # def paperclip_path 16 | # ":rails_root/public/uploads/:id/:attachment/:style_:basename.:extension" 17 | # end 18 | # end 19 | # 20 | # --- 21 | # 22 | # This file contains code taken from Paperclip 23 | # 24 | # LICENSE 25 | # 26 | # The MIT License 27 | # 28 | # Copyright (c) 2008 Jon Yurek and thoughtbot, inc. 29 | # 30 | # Permission is hereby granted, free of charge, to any person obtaining a copy 31 | # of this software and associated documentation files (the "Software"), to deal 32 | # in the Software without restriction, including without limitation the rights 33 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | # copies of the Software, and to permit persons to whom the Software is 35 | # furnished to do so, subject to the following conditions: 36 | # 37 | # The above copyright notice and this permission notice shall be included in 38 | # all copies or substantial portions of the Software. 39 | # 40 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | # THE SOFTWARE. 47 | # 48 | module Paperclip 49 | 50 | def store_path(for_file=filename) 51 | path = paperclip_path 52 | path ||= File.join(*[store_dir, paperclip_style.to_s, for_file].compact) 53 | interpolate_paperclip_path(path, for_file) 54 | end 55 | 56 | def store_dir 57 | ":rails_root/public/system/:attachment/:id" 58 | end 59 | 60 | def paperclip_default_style 61 | :original 62 | end 63 | 64 | def paperclip_path 65 | end 66 | 67 | def paperclip_style 68 | version_name || paperclip_default_style 69 | end 70 | 71 | private 72 | 73 | def interpolate_paperclip_path(path, filename) 74 | mappings.inject(path) do |agg, pair| 75 | agg.gsub(":#{pair[0]}") { pair[1].call(self, filename).to_s } 76 | end 77 | end 78 | 79 | def mappings 80 | [ 81 | [:rails_root , lambda{|u, f| Rails.root }], 82 | [:rails_env , lambda{|u, f| Rails.env }], 83 | [:class , lambda{|u, f| u.model.class.name.underscore.pluralize}], 84 | [:id_partition , lambda{|u, f| ("%09d" % u.model.id).scan(/\d{3}/).join("/")}], 85 | [:id , lambda{|u, f| u.model.id }], 86 | [:attachment , lambda{|u, f| u.mounted_as.to_s.downcase.pluralize }], 87 | [:style , lambda{|u, f| u.paperclip_style }], 88 | [:basename , lambda{|u, f| f.gsub(/#{File.extname(f)}$/, "") }], 89 | [:extension , lambda{|u, f| File.extname(f).gsub(/^\.+/, "")}] 90 | ] 91 | end 92 | 93 | end # Paperclip 94 | end # Compatibility 95 | end # CarrierWave 96 | -------------------------------------------------------------------------------- /spec/processing/mini_magick_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::MiniMagick do 6 | 7 | before do 8 | @klass = Class.new do 9 | include CarrierWave::MiniMagick 10 | end 11 | @instance = @klass.new 12 | FileUtils.cp(file_path('landscape.jpg'), file_path('landscape_copy.jpg')) 13 | @instance.stub(:current_path).and_return(file_path('landscape_copy.jpg')) 14 | @instance.stub(:cached?).and_return true 15 | end 16 | 17 | after do 18 | FileUtils.rm(file_path('landscape_copy.jpg')) 19 | end 20 | 21 | describe "#convert" do 22 | it "should convert from one format to another" do 23 | @instance.convert('png') 24 | img = ::MiniMagick::Image.open(@instance.current_path) 25 | img['format'].should =~ /PNG/ 26 | end 27 | end 28 | 29 | describe '#resize_to_fill' do 30 | it "should resize the image to exactly the given dimensions" do 31 | @instance.resize_to_fill(200, 200) 32 | @instance.should have_dimensions(200, 200) 33 | end 34 | 35 | it "should scale up the image if it smaller than the given dimensions" do 36 | @instance.resize_to_fill(1000, 1000) 37 | @instance.should have_dimensions(1000, 1000) 38 | end 39 | end 40 | 41 | describe '#resize_and_pad' do 42 | it "should resize the image to exactly the given dimensions" do 43 | @instance.resize_and_pad(200, 200) 44 | @instance.should have_dimensions(200, 200) 45 | end 46 | 47 | it "should scale up the image if it smaller than the given dimensions" do 48 | @instance.resize_and_pad(1000, 1000) 49 | @instance.should have_dimensions(1000, 1000) 50 | end 51 | 52 | it "should pad with white" do 53 | @instance.resize_and_pad(200, 200) 54 | image = ::MiniMagick::Image.open(@instance.current_path) 55 | x, y = 0, 0 56 | color = image.run_command("convert", "#{image.escaped_path}[1x1+#{x}+#{y}]", "-depth 8", "txt:").split("\n")[1] 57 | color.should include('#FFFFFF') 58 | end 59 | 60 | end 61 | 62 | describe '#resize_to_fit' do 63 | it "should resize the image to fit within the given dimensions" do 64 | @instance.resize_to_fit(200, 200) 65 | @instance.should have_dimensions(200, 150) 66 | end 67 | 68 | it "should scale up the image if it smaller than the given dimensions" do 69 | @instance.resize_to_fit(1000, 1000) 70 | @instance.should have_dimensions(1000, 750) 71 | end 72 | end 73 | 74 | describe '#resize_to_limit' do 75 | it "should resize the image to fit within the given dimensions" do 76 | @instance.resize_to_limit(200, 200) 77 | @instance.should have_dimensions(200, 150) 78 | end 79 | 80 | it "should not scale up the image if it smaller than the given dimensions" do 81 | @instance.resize_to_limit(1000, 1000) 82 | @instance.should have_dimensions(640, 480) 83 | end 84 | end 85 | 86 | describe "test errors" do 87 | context "invalid image file" do 88 | before do 89 | File.open(@instance.current_path, 'w') do |f| 90 | f.puts "bogus" 91 | end 92 | end 93 | 94 | it "should fail to process a non image file" do 95 | lambda {@instance.resize_to_limit(200, 200)}.should raise_exception(CarrierWave::ProcessingError, /^Failed to manipulate with MiniMagick, maybe it is not an image\? Original Error:/) 96 | end 97 | 98 | it "should use I18n" do 99 | change_locale_and_store_translations(:nl, :errors => { 100 | :messages => { 101 | :mini_magick_processing_error => "Kon bestand niet met MiniMagick bewerken, misschien is het geen beeld bestand? MiniMagick foutmelding: %{e}" 102 | } 103 | }) do 104 | lambda {@instance.resize_to_limit(200, 200)}.should raise_exception(CarrierWave::ProcessingError, /^Kon bestand niet met MiniMagick bewerken, misschien is het geen beeld bestand\? MiniMagick foutmelding:/) 105 | end 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /features/versions_nested_versions.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with nested versions 2 | In order to optimize performance for processing 3 | As a developer using CarrierWave 4 | I want to set nested versions 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | And that the uploader class has a version named 'thumb' 9 | And yo dawg, I put a version called 'mini' in your version called 'thumb' 10 | And yo dawg, I put a version called 'micro' in your version called 'thumb' 11 | And an instance of that class 12 | 13 | Scenario: store a file 14 | When I store the file 'fixtures/bork.txt' 15 | Then there should be a file at 'public/uploads/bork.txt' 16 | Then there should be a file at 'public/uploads/thumb_bork.txt' 17 | Then there should be a file at 'public/uploads/thumb_mini_bork.txt' 18 | Then there should be a file at 'public/uploads/thumb_micro_bork.txt' 19 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 20 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/bork.txt' 21 | And the file at 'public/uploads/thumb_mini_bork.txt' should be identical to the file at 'fixtures/bork.txt' 22 | And the file at 'public/uploads/thumb_micro_bork.txt' should be identical to the file at 'fixtures/bork.txt' 23 | And the uploader should have the url '/uploads/bork.txt' 24 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 25 | And the uploader's nested version 'mini' nested in 'thumb' should have the url '/uploads/thumb_mini_bork.txt' 26 | And the uploader's nested version 'micro' nested in 'thumb' should have the url '/uploads/thumb_micro_bork.txt' 27 | 28 | Scenario: cache a file and then store it 29 | When I cache the file 'fixtures/bork.txt' 30 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 31 | Then there should be a file called 'thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 32 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 33 | And there should not be a file at 'public/uploads/bork.txt' 34 | And there should not be a file at 'public/uploads/thumb_bork.txt' 35 | When I store the file 36 | Then there should be a file at 'public/uploads/bork.txt' 37 | And there should be a file at 'public/uploads/thumb_bork.txt' 38 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 39 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/bork.txt' 40 | And the uploader should have the url '/uploads/bork.txt' 41 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 42 | And the uploader's nested version 'mini' nested in 'thumb' should have the url '/uploads/thumb_mini_bork.txt' 43 | And the uploader's nested version 'micro' nested in 'thumb' should have the url '/uploads/thumb_micro_bork.txt' 44 | 45 | Scenario: retrieving a file from cache then storing 46 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 47 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/thumb_bork.txt' 48 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/thumb_mini_bork.txt' 49 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/thumb_micro_bork.txt' 50 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 51 | And I store the file 52 | Then there should be a file at 'public/uploads/bork.txt' 53 | Then there should be a file at 'public/uploads/thumb_bork.txt' 54 | Then there should be a file at 'public/uploads/thumb_mini_bork.txt' 55 | Then there should be a file at 'public/uploads/thumb_micro_bork.txt' 56 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 57 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 58 | And the file at 'public/uploads/thumb_mini_bork.txt' should be identical to the file at 'fixtures/bork.txt' 59 | And the file at 'public/uploads/thumb_micro_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 60 | 61 | Scenario: retrieving a file from store 62 | Given the file 'fixtures/bork.txt' is stored at 'public/uploads/bork.txt' 63 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_bork.txt' 64 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_mini_bork.txt' 65 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_micro_bork.txt' 66 | When I retrieve the file 'bork.txt' from the store 67 | Then the uploader should have the url '/uploads/bork.txt' 68 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 69 | And the uploader's nested version 'mini' nested in 'thumb' should have the url '/uploads/thumb_mini_bork.txt' 70 | And the uploader's nested version 'micro' nested in 'thumb' should have the url '/uploads/thumb_micro_bork.txt' 71 | -------------------------------------------------------------------------------- /spec/processing/rmagick_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::RMagick do 6 | 7 | before do 8 | @klass = Class.new do 9 | include CarrierWave::RMagick 10 | end 11 | @instance = @klass.new 12 | FileUtils.cp(file_path('landscape.jpg'), file_path('landscape_copy.jpg')) 13 | @instance.stub(:current_path).and_return(file_path('landscape_copy.jpg')) 14 | @instance.stub(:cached?).and_return true 15 | end 16 | 17 | after do 18 | FileUtils.rm(file_path('landscape_copy.jpg')) 19 | end 20 | 21 | describe '#convert' do 22 | it "should convert the image to the given format" do 23 | # TODO: find some way to spec this 24 | @instance.convert(:png) 25 | end 26 | end 27 | 28 | describe '#resize_to_fill' do 29 | it "should resize the image to exactly the given dimensions" do 30 | @instance.resize_to_fill(200, 200) 31 | @instance.should have_dimensions(200, 200) 32 | end 33 | 34 | it "should scale up the image if it smaller than the given dimensions" do 35 | @instance.resize_to_fill(1000, 1000) 36 | @instance.should have_dimensions(1000, 1000) 37 | end 38 | end 39 | 40 | describe '#resize_and_pad' do 41 | it "should resize the image to exactly the given dimensions" do 42 | @instance.resize_and_pad(200, 200) 43 | @instance.should have_dimensions(200, 200) 44 | end 45 | 46 | it "should scale up the image if it smaller than the given dimensions" do 47 | @instance.resize_and_pad(1000, 1000) 48 | @instance.should have_dimensions(1000, 1000) 49 | end 50 | end 51 | 52 | describe '#resize_to_fit' do 53 | it "should resize the image to fit within the given dimensions" do 54 | @instance.resize_to_fit(200, 200) 55 | @instance.should have_dimensions(200, 150) 56 | end 57 | 58 | it "should scale up the image if it smaller than the given dimensions" do 59 | @instance.resize_to_fit(1000, 1000) 60 | @instance.should have_dimensions(1000, 750) 61 | end 62 | end 63 | 64 | describe '#resize_to_limit' do 65 | it "should resize the image to fit within the given dimensions" do 66 | @instance.resize_to_limit(200, 200) 67 | @instance.should have_dimensions(200, 150) 68 | end 69 | 70 | it "should not scale up the image if it smaller than the given dimensions" do 71 | @instance.resize_to_limit(1000, 1000) 72 | @instance.should have_dimensions(640, 480) 73 | end 74 | end 75 | 76 | describe '#resize_to_geometry_string' do 77 | it "should resize the image to comply with `200x200^` Geometry String spec" do 78 | @instance.resize_to_geometry_string('200x200^') 79 | @instance.should have_dimensions(267, 200) 80 | end 81 | 82 | it "should resize the image to have 125% larger dimensions" do 83 | @instance.resize_to_geometry_string('125%') 84 | @instance.should have_dimensions(800, 600) 85 | end 86 | 87 | it "should resize the image to have a given height" do 88 | @instance.resize_to_geometry_string('x256') 89 | @instance.should have_height(256) 90 | end 91 | 92 | it "should resize the image to have a given width" do 93 | @instance.resize_to_geometry_string('256x') 94 | @instance.should have_width(256) 95 | end 96 | end 97 | 98 | describe "#manipulate!" do 99 | it 'should support passing write options to RMagick' do 100 | image = ::Magick::Image.read(file_path('landscape_copy.jpg')) 101 | ::Magick::Image.stub(:read => image) 102 | ::Magick::Image::Info.any_instance.should_receive(:quality=).with(50) 103 | ::Magick::Image::Info.any_instance.should_receive(:depth=).with(8) 104 | 105 | @instance.manipulate! do |image, index, options| 106 | options[:write] = { 107 | :quality => 50, 108 | :depth => 8 109 | } 110 | image 111 | end 112 | end 113 | 114 | it 'should support passing read options to RMagick' do 115 | ::Magick::Image::Info.any_instance.should_receive(:density=).with(10) 116 | ::Magick::Image::Info.any_instance.should_receive(:size=).with("200x200") 117 | 118 | @instance.manipulate! :read => { 119 | :density => 10, 120 | :size => %{"200x200"} 121 | } 122 | end 123 | end 124 | 125 | describe "test errors" do 126 | context "invalid image file" do 127 | before do 128 | File.open(@instance.current_path, 'w') do |f| 129 | f.puts "bogus" 130 | end 131 | end 132 | 133 | it "should fail to process a non image file" do 134 | lambda {@instance.resize_to_limit(200, 200)}.should raise_exception(CarrierWave::ProcessingError, /^Failed to manipulate with rmagick, maybe it is not an image\? Original Error:/) 135 | end 136 | 137 | it "should use I18n" do 138 | change_locale_and_store_translations(:nl, :errors => { 139 | :messages => { 140 | :rmagick_processing_error => "Kon bestand niet met rmagick bewerken, misschien is het geen beeld bestand? rmagick foutmelding: %{e}" 141 | } 142 | }) do 143 | lambda {@instance.resize_to_limit(200, 200)}.should raise_exception(CarrierWave::ProcessingError, /^Kon bestand niet met rmagick bewerken, misschien is het geen beeld bestand\? rmagick foutmelding:/) 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/configuration.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | 3 | module Uploader 4 | module Configuration 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | class_attribute :_storage, :instance_writer => false 9 | 10 | add_config :root 11 | add_config :base_path 12 | add_config :asset_host 13 | add_config :permissions 14 | add_config :directory_permissions 15 | add_config :storage_engines 16 | add_config :store_dir 17 | add_config :cache_dir 18 | add_config :enable_processing 19 | add_config :ensure_multipart_form 20 | add_config :delete_tmp_file_after_storage 21 | add_config :move_to_cache 22 | add_config :move_to_store 23 | add_config :remove_previously_stored_files_after_update 24 | 25 | # fog 26 | add_config :fog_attributes 27 | add_config :fog_credentials 28 | add_config :fog_directory 29 | add_config :fog_endpoint 30 | add_config :fog_public 31 | add_config :fog_authenticated_url_expiration 32 | 33 | # Mounting 34 | add_config :ignore_integrity_errors 35 | add_config :ignore_processing_errors 36 | add_config :ignore_download_errors 37 | add_config :validate_integrity 38 | add_config :validate_processing 39 | add_config :validate_download 40 | add_config :mount_on 41 | 42 | # set default values 43 | reset_config 44 | end 45 | 46 | module ClassMethods 47 | 48 | ## 49 | # Sets the storage engine to be used when storing files with this uploader. 50 | # Can be any class that implements a #store!(CarrierWave::SanitizedFile) and a #retrieve! 51 | # method. See lib/carrierwave/storage/file.rb for an example. Storage engines should 52 | # be added to CarrierWave::Uploader::Base.storage_engines so they can be referred 53 | # to by a symbol, which should be more convenient 54 | # 55 | # If no argument is given, it will simply return the currently used storage engine. 56 | # 57 | # === Parameters 58 | # 59 | # [storage (Symbol, Class)] The storage engine to use for this uploader 60 | # 61 | # === Returns 62 | # 63 | # [Class] the storage engine to be used with this uploader 64 | # 65 | # === Examples 66 | # 67 | # storage :file 68 | # storage CarrierWave::Storage::File 69 | # storage MyCustomStorageEngine 70 | # 71 | def storage(storage = nil) 72 | if storage 73 | self._storage = storage.is_a?(Symbol) ? eval(storage_engines[storage]) : storage 74 | end 75 | _storage 76 | end 77 | alias_method :storage=, :storage 78 | 79 | def add_config(name) 80 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 81 | def self.#{name}(value=nil) 82 | @#{name} = value if value 83 | return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name}) 84 | name = superclass.#{name} 85 | return nil if name.nil? && !instance_variable_defined?("@#{name}") 86 | @#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name 87 | end 88 | 89 | def self.#{name}=(value) 90 | @#{name} = value 91 | end 92 | 93 | def #{name}=(value) 94 | @#{name} = value 95 | end 96 | 97 | def #{name} 98 | value = @#{name} if instance_variable_defined?(:@#{name}) 99 | value = self.class.#{name} unless instance_variable_defined?(:@#{name}) 100 | value.instance_of?(Proc) ? value.call : value 101 | end 102 | RUBY 103 | end 104 | 105 | def configure 106 | yield self 107 | end 108 | 109 | ## 110 | # sets configuration back to default 111 | # 112 | def reset_config 113 | configure do |config| 114 | config.permissions = 0644 115 | config.directory_permissions = 0755 116 | config.storage_engines = { 117 | :file => "CarrierWave::Storage::File", 118 | :fog => "CarrierWave::Storage::Fog" 119 | } 120 | config.storage = :file 121 | config.fog_attributes = {} 122 | config.fog_credentials = {} 123 | config.fog_public = true 124 | config.fog_authenticated_url_expiration = 600 125 | config.store_dir = 'uploads' 126 | config.cache_dir = 'uploads/tmp' 127 | config.delete_tmp_file_after_storage = true 128 | config.move_to_cache = false 129 | config.move_to_store = false 130 | config.remove_previously_stored_files_after_update = true 131 | config.ignore_integrity_errors = true 132 | config.ignore_processing_errors = true 133 | config.ignore_download_errors = true 134 | config.validate_integrity = true 135 | config.validate_processing = true 136 | config.validate_download = true 137 | config.root = lambda { CarrierWave.root } 138 | config.base_path = CarrierWave.base_path 139 | config.enable_processing = true 140 | config.ensure_multipart_form = true 141 | end 142 | end 143 | end 144 | 145 | end 146 | end 147 | end 148 | 149 | -------------------------------------------------------------------------------- /spec/uploader/download_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader::Download do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe '#download!' do 17 | 18 | before do 19 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 20 | 21 | sham_rack_app = ShamRack.at('www.example.com').stub 22 | sham_rack_app.register_resource('/test.jpg', File.read(file_path('test.jpg')), 'image/jpg') 23 | sham_rack_app.register_resource('/test%20with%20spaces/test.jpg', File.read(file_path('test.jpg')), 'image/jpg') 24 | 25 | ShamRack.at("www.redirect.com") do |env| 26 | [301, {'Content-Type'=>'text/html', 'Location'=>"http://www.example.com/test.jpg"}, ['Redirecting']] 27 | end 28 | end 29 | 30 | after do 31 | ShamRack.unmount_all 32 | end 33 | 34 | it "should cache a file" do 35 | @uploader.download!('http://www.example.com/test.jpg') 36 | @uploader.file.should be_an_instance_of(CarrierWave::SanitizedFile) 37 | end 38 | 39 | it "should be cached" do 40 | @uploader.download!('http://www.example.com/test.jpg') 41 | @uploader.should be_cached 42 | end 43 | 44 | it "should store the cache name" do 45 | @uploader.download!('http://www.example.com/test.jpg') 46 | @uploader.cache_name.should == '20071201-1234-345-2255/test.jpg' 47 | end 48 | 49 | it "should set the filename to the file's sanitized filename" do 50 | @uploader.download!('http://www.example.com/test.jpg') 51 | @uploader.filename.should == 'test.jpg' 52 | end 53 | 54 | it "should move it to the tmp dir" do 55 | @uploader.download!('http://www.example.com/test.jpg') 56 | @uploader.file.path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 57 | @uploader.file.exists?.should be_true 58 | end 59 | 60 | it "should set the url" do 61 | @uploader.download!('http://www.example.com/test.jpg') 62 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 63 | end 64 | 65 | it "should do nothing when trying to download an empty file" do 66 | @uploader.download!(nil) 67 | end 68 | 69 | it "should set permissions if options are given" do 70 | @uploader_class.permissions = 0777 71 | 72 | @uploader.download!('http://www.example.com/test.jpg') 73 | @uploader.should have_permissions(0777) 74 | end 75 | 76 | it "should set directory permissions if options are given" do 77 | @uploader_class.directory_permissions = 0777 78 | 79 | @uploader.download!('http://www.example.com/test.jpg') 80 | @uploader.should have_directory_permissions(0777) 81 | end 82 | 83 | it "should raise an error when trying to download a local file" do 84 | running { 85 | @uploader.download!('/etc/passwd') 86 | }.should raise_error(CarrierWave::DownloadError) 87 | end 88 | 89 | it "should raise an error when trying to download a missing file" do 90 | running { 91 | @uploader.download!('http://www.example.com/missing.jpg') 92 | }.should raise_error(CarrierWave::DownloadError) 93 | end 94 | 95 | it "should accept spaces in the url" do 96 | @uploader.download!('http://www.example.com/test with spaces/test.jpg') 97 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 98 | end 99 | 100 | it "should follow redirects" do 101 | @uploader.download!('http://www.redirect.com/') 102 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 103 | end 104 | 105 | describe '#download! with an extension_white_list' do 106 | before do 107 | @uploader_class.class_eval do 108 | def extension_white_list 109 | %w(txt) 110 | end 111 | end 112 | end 113 | 114 | it "should follow redirects but still respect the extension_white_list" do 115 | running { 116 | @uploader.download!('http://www.redirect.com/') 117 | }.should raise_error(CarrierWave::IntegrityError) 118 | end 119 | end 120 | 121 | describe '#download! with an extension_black_list' do 122 | before do 123 | @uploader_class.class_eval do 124 | def extension_black_list 125 | %w(jpg) 126 | end 127 | end 128 | end 129 | 130 | it "should follow redirects but still respect the extension_black_list" do 131 | running { 132 | @uploader.download!('http://www.redirect.com/') 133 | }.should raise_error(CarrierWave::IntegrityError) 134 | end 135 | end 136 | end 137 | 138 | describe '#download! with an overridden process_uri method' do 139 | before do 140 | @uploader_class.class_eval do 141 | def process_uri(uri) 142 | raise CarrierWave::DownloadError 143 | end 144 | end 145 | end 146 | 147 | it "should allow overriding the process_uri method" do 148 | running { 149 | @uploader.download!('http://www.example.com/test.jpg') 150 | }.should raise_error(CarrierWave::DownloadError) 151 | end 152 | end 153 | 154 | describe '#process_uri' do 155 | let(:uri) { "http://www.example.com/test%20image.jpg" } 156 | 157 | it 'should unescape and then escape the given uri' do 158 | unescaped_uri = URI.unescape(uri) 159 | @uploader.process_uri(unescaped_uri).should == @uploader.process_uri(uri) 160 | end 161 | 162 | it 'should parse the given uri' do 163 | @uploader.process_uri(uri).should == URI.parse(uri) 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/uploader/processing_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 9 | @uploader = @uploader_class.new 10 | end 11 | 12 | after do 13 | FileUtils.rm_rf(public_path) 14 | end 15 | 16 | describe '.process' do 17 | it "should add a single processor when a symbol is given" do 18 | @uploader_class.process :sepiatone 19 | @uploader.should_receive(:sepiatone) 20 | @uploader.process! 21 | end 22 | 23 | it "should add multiple processors when an array of symbols is given" do 24 | @uploader_class.process :sepiatone, :desaturate, :invert 25 | @uploader.should_receive(:sepiatone) 26 | @uploader.should_receive(:desaturate) 27 | @uploader.should_receive(:invert) 28 | @uploader.process! 29 | end 30 | 31 | it "should add a single processor with an argument when a hash is given" do 32 | @uploader_class.process :format => 'png' 33 | @uploader.should_receive(:format).with('png') 34 | @uploader.process! 35 | end 36 | 37 | it "should add a single processor with several argument when a hash is given" do 38 | @uploader_class.process :resize => [200, 300] 39 | @uploader.should_receive(:resize).with(200, 300) 40 | @uploader.process! 41 | end 42 | 43 | it "should add multiple processors when an hash with multiple keys is given" do 44 | @uploader_class.process :resize => [200, 300], :format => 'png' 45 | @uploader.should_receive(:resize).with(200, 300) 46 | @uploader.should_receive(:format).with('png') 47 | @uploader.process! 48 | end 49 | 50 | it "should call the processor if the condition method returns true" do 51 | @uploader_class.process :resize => [200, 300], :if => :true? 52 | @uploader_class.process :fancy, :if => :true? 53 | @uploader.should_receive(:true?).with("test.jpg").twice.and_return(true) 54 | @uploader.should_receive(:resize).with(200, 300) 55 | @uploader.should_receive(:fancy).with() 56 | @uploader.process!("test.jpg") 57 | end 58 | 59 | it "should not call the processor if the condition method returns false" do 60 | @uploader_class.process :resize => [200, 300], :if => :false? 61 | @uploader_class.process :fancy, :if => :false? 62 | @uploader.should_receive(:false?).with("test.jpg").twice.and_return(false) 63 | @uploader.should_not_receive(:resize) 64 | @uploader.should_not_receive(:fancy) 65 | @uploader.process!("test.jpg") 66 | end 67 | 68 | it "should call the processor if the condition block returns true" do 69 | @uploader_class.process :resize => [200, 300], :if => lambda{|record, args| record.true?(args[:file])} 70 | @uploader_class.process :fancy, :if => :true? 71 | @uploader.should_receive(:true?).with("test.jpg").twice.and_return(true) 72 | @uploader.should_receive(:resize).with(200, 300) 73 | @uploader.should_receive(:fancy).with() 74 | @uploader.process!("test.jpg") 75 | end 76 | 77 | it "should not call the processor if the condition block returns false" do 78 | @uploader_class.process :resize => [200, 300], :if => lambda{|record, args| record.false?(args[:file])} 79 | @uploader_class.process :fancy, :if => :false? 80 | @uploader.should_receive(:false?).with("test.jpg").twice.and_return(false) 81 | @uploader.should_not_receive(:resize) 82 | @uploader.should_not_receive(:fancy) 83 | @uploader.process!("test.jpg") 84 | end 85 | 86 | context "when using RMagick" do 87 | before do 88 | def @uploader.cover 89 | manipulate! { |frame, index| frame if index.zero? } 90 | end 91 | 92 | @uploader_class.send :include, CarrierWave::RMagick 93 | end 94 | 95 | after do 96 | @uploader.instance_eval { undef cover } 97 | end 98 | 99 | context "with a multi-page PDF" do 100 | before do 101 | @uploader.cache! File.open(file_path("multi_page.pdf")) 102 | end 103 | 104 | it "should successfully process" do 105 | @uploader_class.process :convert => 'jpg' 106 | @uploader.process! 107 | end 108 | 109 | it "should support page specific transformations" do 110 | @uploader_class.process :cover 111 | @uploader.process! 112 | end 113 | end 114 | 115 | context "with a simple image" do 116 | before do 117 | @uploader.cache! File.open(file_path("portrait.jpg")) 118 | end 119 | 120 | it "should still allow page specific transformations" do 121 | @uploader_class.process :cover 122 | @uploader.process! 123 | end 124 | end 125 | end 126 | 127 | context "with 'enable_processing' set to false" do 128 | it "should not do any processing" do 129 | @uploader_class.enable_processing = false 130 | @uploader_class.process :sepiatone, :desaturate, :invert 131 | @uploader.should_not_receive(:sepiatone) 132 | @uploader.should_not_receive(:desaturate) 133 | @uploader.should_not_receive(:invert) 134 | @uploader.process! 135 | end 136 | end 137 | end 138 | 139 | describe '#cache!' do 140 | before do 141 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 142 | end 143 | 144 | it "should trigger a process!" do 145 | @uploader.should_receive(:process!) 146 | @uploader.cache!(File.open(file_path('test.jpg'))) 147 | end 148 | end 149 | 150 | describe '#recreate_versions!' do 151 | before do 152 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 153 | end 154 | 155 | it "should trigger a process!" do 156 | @uploader.store!(File.open(file_path('test.jpg'))) 157 | @uploader.should_receive(:process!) 158 | @uploader.recreate_versions! 159 | end 160 | end 161 | 162 | end 163 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/cache.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | 5 | class FormNotMultipart < UploadError 6 | def message 7 | "You tried to assign a String or a Pathname to an uploader, for security reasons, this is not allowed.\n\n If this is a file upload, please check that your upload form is multipart encoded." 8 | end 9 | end 10 | 11 | ## 12 | # Generates a unique cache id for use in the caching system 13 | # 14 | # === Returns 15 | # 16 | # [String] a cache id in the format YYYYMMDD-HHMM-PID-RND 17 | # 18 | def self.generate_cache_id 19 | Time.now.strftime('%Y%m%d-%H%M') + '-' + Process.pid.to_s + '-' + ("%04d" % rand(9999)) 20 | end 21 | 22 | module Uploader 23 | module Cache 24 | extend ActiveSupport::Concern 25 | 26 | include CarrierWave::Uploader::Callbacks 27 | include CarrierWave::Uploader::Configuration 28 | 29 | module ClassMethods 30 | 31 | ## 32 | # Removes cached files which are older than one day. You could call this method 33 | # from a rake task to clean out old cached files. 34 | # 35 | # You can call this method directly on the module like this: 36 | # 37 | # CarrierWave.clean_cached_files! 38 | # 39 | # === Note 40 | # 41 | # This only works as long as you haven't done anything funky with your cache_dir. 42 | # It's recommended that you keep cache files in one place only. 43 | # 44 | def clean_cached_files!(seconds=60*60*24) 45 | Dir.glob(File.expand_path(File.join(cache_dir, '*'), CarrierWave.root)).each do |dir| 46 | time = dir.scan(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/).first.map { |t| t.to_i } 47 | time = Time.utc(*time) 48 | if time < (Time.now.utc - seconds) 49 | FileUtils.rm_rf(dir) 50 | end 51 | end 52 | end 53 | end 54 | 55 | ## 56 | # Returns true if the uploader has been cached 57 | # 58 | # === Returns 59 | # 60 | # [Bool] whether the current file is cached 61 | # 62 | def cached? 63 | @cache_id 64 | end 65 | 66 | ## 67 | # Caches the remotely stored file 68 | # 69 | # This is useful when about to process images. Most processing solutions 70 | # require the file to be stored on the local filesystem. 71 | # 72 | def cache_stored_file! 73 | cache! 74 | end 75 | 76 | def sanitized_file 77 | _content = file.read 78 | if _content.is_a?(File) # could be if storage is Fog 79 | sanitized = CarrierWave::Storage::Fog.new(self).retrieve!(File.basename(_content.path)) 80 | sanitized.read if sanitized.exists? 81 | 82 | else 83 | sanitized = SanitizedFile.new :tempfile => StringIO.new(file.read), 84 | :filename => File.basename(path), :content_type => file.content_type 85 | end 86 | sanitized 87 | end 88 | 89 | ## 90 | # Returns a String which uniquely identifies the currently cached file for later retrieval 91 | # 92 | # === Returns 93 | # 94 | # [String] a cache name, in the format YYYYMMDD-HHMM-PID-RND/filename.txt 95 | # 96 | def cache_name 97 | File.join(cache_id, full_original_filename) if cache_id and original_filename 98 | end 99 | 100 | ## 101 | # Caches the given file. Calls process! to trigger any process callbacks. 102 | # 103 | # By default, cache!() uses copy_to(), which operates by copying the file 104 | # to the cache, then deleting the original file. If move_to_cache() is 105 | # overriden to return true, then cache!() uses move_to(), which simply 106 | # moves the file to the cache. Useful for large files. 107 | # 108 | # === Parameters 109 | # 110 | # [new_file (File, IOString, Tempfile)] any kind of file object 111 | # 112 | # === Raises 113 | # 114 | # [CarrierWave::FormNotMultipart] if the assigned parameter is a string 115 | # 116 | def cache!(new_file = sanitized_file) 117 | new_file = CarrierWave::SanitizedFile.new(new_file) 118 | 119 | unless new_file.empty? 120 | raise CarrierWave::FormNotMultipart if new_file.is_path? && ensure_multipart_form 121 | 122 | with_callbacks(:cache, new_file) do 123 | self.cache_id = CarrierWave.generate_cache_id unless cache_id 124 | 125 | @filename = new_file.filename 126 | self.original_filename = new_file.filename 127 | 128 | if move_to_cache 129 | @file = new_file.move_to(cache_path, permissions, directory_permissions) 130 | else 131 | @file = new_file.copy_to(cache_path, permissions, directory_permissions) 132 | end 133 | end 134 | end 135 | end 136 | 137 | ## 138 | # Retrieves the file with the given cache_name from the cache. 139 | # 140 | # === Parameters 141 | # 142 | # [cache_name (String)] uniquely identifies a cache file 143 | # 144 | # === Raises 145 | # 146 | # [CarrierWave::InvalidParameter] if the cache_name is incorrectly formatted. 147 | # 148 | def retrieve_from_cache!(cache_name) 149 | with_callbacks(:retrieve_from_cache, cache_name) do 150 | self.cache_id, self.original_filename = cache_name.to_s.split('/', 2) 151 | @filename = original_filename 152 | @file = CarrierWave::SanitizedFile.new(cache_path) 153 | end 154 | end 155 | 156 | private 157 | 158 | def cache_path 159 | File.expand_path(File.join(cache_dir, cache_name), root) 160 | end 161 | 162 | attr_reader :cache_id, :original_filename 163 | 164 | # We can override the full_original_filename method in other modules 165 | alias_method :full_original_filename, :original_filename 166 | 167 | def cache_id=(cache_id) 168 | raise CarrierWave::InvalidParameter, "invalid cache id" unless cache_id =~ /\A[\d]{8}\-[\d]{4}\-[\d]+\-[\d]{4}\z/ 169 | @cache_id = cache_id 170 | end 171 | 172 | def original_filename=(filename) 173 | raise CarrierWave::InvalidParameter, "invalid filename" if filename =~ CarrierWave::SanitizedFile.sanitize_regexp 174 | @original_filename = filename 175 | end 176 | 177 | end # Cache 178 | end # Uploader 179 | end # CarrierWave 180 | -------------------------------------------------------------------------------- /spec/uploader/url_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'active_support/json' 5 | 6 | describe CarrierWave::Uploader do 7 | 8 | before do 9 | class MyCoolUploader < CarrierWave::Uploader::Base; end 10 | @uploader = MyCoolUploader.new 11 | end 12 | 13 | after do 14 | FileUtils.rm_rf(public_path) 15 | Object.send(:remove_const, "MyCoolUploader") if defined?(::MyCoolUploader) 16 | end 17 | 18 | describe '#url' do 19 | before do 20 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 21 | end 22 | 23 | it "should default to nil" do 24 | @uploader.url.should be_nil 25 | end 26 | 27 | it "should raise ArgumentError when version doesn't exist" do 28 | lambda { @uploader.url(:thumb) }.should raise_error(ArgumentError) 29 | end 30 | 31 | it "should not raise exception when hash specified as argument" do 32 | lambda { @uploader.url({}) }.should_not raise_error 33 | end 34 | 35 | it "should not raise ArgumentError when storage's File#url method doesn't get params" do 36 | module StorageX; class File; def url; true; end; end; end 37 | @uploader.stub!(:file).and_return(StorageX::File.new) 38 | lambda { @uploader.url }.should_not raise_error 39 | end 40 | 41 | it "should not raise ArgumentError when versions version exists" do 42 | MyCoolUploader.version(:thumb) 43 | lambda { @uploader.url(:thumb) }.should_not raise_error(ArgumentError) 44 | end 45 | 46 | it "should get the directory relative to public, prepending a slash" do 47 | @uploader.cache!(File.open(file_path('test.jpg'))) 48 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 49 | end 50 | 51 | it "should get the directory relative to public for a specific version" do 52 | MyCoolUploader.version(:thumb) 53 | @uploader.cache!(File.open(file_path('test.jpg'))) 54 | @uploader.url(:thumb).should == '/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg' 55 | end 56 | 57 | it "should get the directory relative to public for a nested version" do 58 | MyCoolUploader.version(:thumb) do 59 | version(:mini) 60 | end 61 | @uploader.cache!(File.open(file_path('test.jpg'))) 62 | @uploader.url(:thumb, :mini).should == '/uploads/tmp/20071201-1234-345-2255/thumb_mini_test.jpg' 63 | end 64 | 65 | it "should prepend the config option 'asset_host', if set and a string" do 66 | MyCoolUploader.version(:thumb) 67 | @uploader.class.configure do |config| 68 | config.asset_host = "http://foo.bar" 69 | end 70 | @uploader.cache!(File.open(file_path('test.jpg'))) 71 | @uploader.url(:thumb).should == 'http://foo.bar/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg' 72 | end 73 | 74 | it "should prepend the result of the config option 'asset_host', if set and a proc" do 75 | MyCoolUploader.version(:thumb) 76 | @uploader.class.configure do |config| 77 | config.asset_host = proc { "http://foo.bar" } 78 | end 79 | @uploader.cache!(File.open(file_path('test.jpg'))) 80 | @uploader.url(:thumb).should == 'http://foo.bar/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg' 81 | end 82 | 83 | it "should prepend the config option 'base_path', if set and 'asset_host' is not set" do 84 | MyCoolUploader.version(:thumb) 85 | @uploader.class.configure do |config| 86 | config.base_path = "/base_path" 87 | config.asset_host = nil 88 | end 89 | @uploader.cache!(File.open(file_path('test.jpg'))) 90 | @uploader.url(:thumb).should == '/base_path/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg' 91 | end 92 | 93 | it "should return file#url if available" do 94 | @uploader.cache!(File.open(file_path('test.jpg'))) 95 | @uploader.file.stub!(:url).and_return('http://www.example.com/someurl.jpg') 96 | @uploader.url.should == 'http://www.example.com/someurl.jpg' 97 | end 98 | 99 | it "should get the directory relative to public, if file#url is blank" do 100 | @uploader.cache!(File.open(file_path('test.jpg'))) 101 | @uploader.file.stub!(:url).and_return('') 102 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 103 | end 104 | end 105 | 106 | describe '#to_json' do 107 | before do 108 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 109 | end 110 | 111 | it "should return a hash with a nil URL" do 112 | MyCoolUploader.version(:thumb) 113 | hash = JSON.parse(@uploader.to_json) 114 | hash.keys.should 115 | hash.keys.should include("uploader") 116 | hash["uploader"].keys.should include("url") 117 | hash["uploader"].keys.should include("thumb") 118 | hash["uploader"]["url"].should be_nil 119 | hash["uploader"]["thumb"].keys.should include("url") 120 | hash["uploader"]["thumb"]["url"].should be_nil 121 | end 122 | 123 | it "should return a hash including a cached URL" do 124 | @uploader.cache!(File.open(file_path("test.jpg"))) 125 | JSON.parse(@uploader.to_json).should == {"uploader" => {"url" => "/uploads/tmp/20071201-1234-345-2255/test.jpg"}} 126 | end 127 | 128 | it "should return a hash including a cached URL of a version" do 129 | MyCoolUploader.version(:thumb) 130 | @uploader.cache!(File.open(file_path("test.jpg"))) 131 | hash = JSON.parse(@uploader.to_json)["uploader"] 132 | hash.keys.should include "thumb" 133 | hash["thumb"].should == {"url" => "/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg"} 134 | end 135 | end 136 | 137 | describe '#to_xml' do 138 | before do 139 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 140 | end 141 | 142 | it "should return a hash with a blank URL" do 143 | Hash.from_xml(@uploader.to_xml).should == {"uploader" => {"url" => nil}} 144 | end 145 | 146 | it "should return a hash including a cached URL" do 147 | @uploader.cache!(File.open(file_path("test.jpg"))) 148 | Hash.from_xml(@uploader.to_xml).should == {"uploader" => {"url" => "/uploads/tmp/20071201-1234-345-2255/test.jpg"}} 149 | end 150 | 151 | it "should return a hash including a cached URL of a version" do 152 | MyCoolUploader.version(:thumb) 153 | @uploader.cache!(File.open(file_path("test.jpg"))) 154 | Hash.from_xml(@uploader.to_xml)["uploader"]["thumb"].should == {"url" => "/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg"} 155 | end 156 | 157 | it "should return a hash including an array with a cached URL" do 158 | @uploader.cache!(File.open(file_path("test.jpg"))) 159 | hash = Hash.from_xml([@uploader].to_xml) 160 | hash.should have_value([{"url"=>"/uploads/tmp/20071201-1234-345-2255/test.jpg"}]) 161 | end 162 | end 163 | 164 | describe '#to_s' do 165 | before do 166 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 167 | end 168 | 169 | it "should default to empty space" do 170 | @uploader.to_s.should == '' 171 | end 172 | 173 | it "should get the directory relative to public, prepending a slash" do 174 | @uploader.cache!(File.open(file_path('test.jpg'))) 175 | @uploader.to_s.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 176 | end 177 | 178 | it "should return file#url if available" do 179 | @uploader.cache!(File.open(file_path('test.jpg'))) 180 | @uploader.file.stub!(:url).and_return('http://www.example.com/someurl.jpg') 181 | @uploader.to_s.should == 'http://www.example.com/someurl.jpg' 182 | end 183 | end 184 | 185 | end 186 | -------------------------------------------------------------------------------- /lib/carrierwave/sanitized_file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'pathname' 4 | require 'active_support/core_ext/string/multibyte' 5 | 6 | module CarrierWave 7 | 8 | ## 9 | # SanitizedFile is a base class which provides a common API around all 10 | # the different quirky Ruby File libraries. It has support for Tempfile, 11 | # File, StringIO, Merb-style upload Hashes, as well as paths given as 12 | # Strings and Pathnames. 13 | # 14 | # It's probably needlessly comprehensive and complex. Help is appreciated. 15 | # 16 | class SanitizedFile 17 | 18 | attr_accessor :file 19 | 20 | class << self 21 | attr_writer :sanitize_regexp 22 | 23 | def sanitize_regexp 24 | @sanitize_regexp ||= /[^a-zA-Z0-9\.\-\+_]/ 25 | end 26 | end 27 | 28 | def initialize(file) 29 | self.file = file 30 | end 31 | 32 | ## 33 | # Returns the filename as is, without sanizting it. 34 | # 35 | # === Returns 36 | # 37 | # [String] the unsanitized filename 38 | # 39 | def original_filename 40 | return @original_filename if @original_filename 41 | if @file and @file.respond_to?(:original_filename) 42 | @file.original_filename 43 | elsif path 44 | File.basename(path) 45 | end 46 | end 47 | 48 | ## 49 | # Returns the filename, sanitized to strip out any evil characters. 50 | # 51 | # === Returns 52 | # 53 | # [String] the sanitized filename 54 | # 55 | def filename 56 | sanitize(original_filename) if original_filename 57 | end 58 | 59 | alias_method :identifier, :filename 60 | 61 | ## 62 | # Returns the part of the filename before the extension. So if a file is called 'test.jpeg' 63 | # this would return 'test' 64 | # 65 | # === Returns 66 | # 67 | # [String] the first part of the filename 68 | # 69 | def basename 70 | split_extension(filename)[0] if filename 71 | end 72 | 73 | ## 74 | # Returns the file extension 75 | # 76 | # === Returns 77 | # 78 | # [String] the extension 79 | # 80 | def extension 81 | split_extension(filename)[1] if filename 82 | end 83 | 84 | ## 85 | # Returns the file's size. 86 | # 87 | # === Returns 88 | # 89 | # [Integer] the file's size in bytes. 90 | # 91 | def size 92 | if is_path? 93 | exists? ? File.size(path) : 0 94 | elsif @file.respond_to?(:size) 95 | @file.size 96 | elsif path 97 | exists? ? File.size(path) : 0 98 | else 99 | 0 100 | end 101 | end 102 | 103 | ## 104 | # Returns the full path to the file. If the file has no path, it will return nil. 105 | # 106 | # === Returns 107 | # 108 | # [String, nil] the path where the file is located. 109 | # 110 | def path 111 | unless @file.blank? 112 | if is_path? 113 | File.expand_path(@file) 114 | elsif @file.respond_to?(:path) and not @file.path.blank? 115 | File.expand_path(@file.path) 116 | end 117 | end 118 | end 119 | 120 | ## 121 | # === Returns 122 | # 123 | # [Boolean] whether the file is supplied as a pathname or string. 124 | # 125 | def is_path? 126 | !!((@file.is_a?(String) || @file.is_a?(Pathname)) && !@file.blank?) 127 | end 128 | 129 | ## 130 | # === Returns 131 | # 132 | # [Boolean] whether the file is valid and has a non-zero size 133 | # 134 | def empty? 135 | @file.nil? || self.size.nil? || (self.size.zero? && ! self.exists?) 136 | end 137 | 138 | ## 139 | # === Returns 140 | # 141 | # [Boolean] Whether the file exists 142 | # 143 | def exists? 144 | return File.exists?(self.path) if self.path 145 | return false 146 | end 147 | 148 | ## 149 | # Returns the contents of the file. 150 | # 151 | # === Returns 152 | # 153 | # [String] contents of the file 154 | # 155 | def read 156 | if is_path? 157 | File.open(@file, "rb") {|file| file.read} 158 | else 159 | @file.rewind if @file.respond_to?(:rewind) 160 | @file.read 161 | end 162 | end 163 | 164 | ## 165 | # Moves the file to the given path 166 | # 167 | # === Parameters 168 | # 169 | # [new_path (String)] The path where the file should be moved. 170 | # [permissions (Integer)] permissions to set on the file in its new location. 171 | # [directory_permissions (Integer)] permissions to set on created directories. 172 | # 173 | def move_to(new_path, permissions=nil, directory_permissions=nil) 174 | return if self.empty? 175 | new_path = File.expand_path(new_path) 176 | 177 | mkdir!(new_path, directory_permissions) 178 | if exists? 179 | FileUtils.mv(path, new_path) unless new_path == path 180 | else 181 | File.open(new_path, "wb") { |f| f.write(read) } 182 | end 183 | chmod!(new_path, permissions) 184 | self.file = new_path 185 | self 186 | end 187 | 188 | ## 189 | # Creates a copy of this file and moves it to the given path. Returns the copy. 190 | # 191 | # === Parameters 192 | # 193 | # [new_path (String)] The path where the file should be copied to. 194 | # [permissions (Integer)] permissions to set on the copy 195 | # [directory_permissions (Integer)] permissions to set on created directories. 196 | # 197 | # === Returns 198 | # 199 | # @return [CarrierWave::SanitizedFile] the location where the file will be stored. 200 | # 201 | def copy_to(new_path, permissions=nil, directory_permissions=nil) 202 | return if self.empty? 203 | new_path = File.expand_path(new_path) 204 | 205 | mkdir!(new_path, directory_permissions) 206 | if exists? 207 | FileUtils.cp(path, new_path) unless new_path == path 208 | else 209 | File.open(new_path, "wb") { |f| f.write(read) } 210 | end 211 | chmod!(new_path, permissions) 212 | self.class.new({:tempfile => new_path, :content_type => content_type}) 213 | end 214 | 215 | ## 216 | # Removes the file from the filesystem. 217 | # 218 | def delete 219 | FileUtils.rm(self.path) if exists? 220 | end 221 | 222 | ## 223 | # Returns a File object, or nil if it does not exist. 224 | # 225 | # === Returns 226 | # 227 | # [File] a File object representing the SanitizedFile 228 | # 229 | def to_file 230 | return @file if @file.is_a?(File) 231 | File.open(path, "rb") if exists? 232 | end 233 | 234 | ## 235 | # Returns the content type of the file. 236 | # 237 | # === Returns 238 | # 239 | # [String] the content type of the file 240 | # 241 | def content_type 242 | return @content_type if @content_type 243 | @file.content_type.to_s.chomp if @file.respond_to?(:content_type) and @file.content_type 244 | end 245 | 246 | ## 247 | # Sets the content type of the file. 248 | # 249 | # === Returns 250 | # 251 | # [String] the content type of the file 252 | # 253 | def content_type=(type) 254 | @content_type = type 255 | end 256 | 257 | ## 258 | # Used to sanitize the file name. Public to allow overriding for non-latin characters. 259 | # 260 | # === Returns 261 | # 262 | # [Regexp] the regexp for sanitizing the file name 263 | # 264 | def sanitize_regexp 265 | CarrierWave::SanitizedFile.sanitize_regexp 266 | end 267 | 268 | private 269 | 270 | def file=(file) 271 | if file.is_a?(Hash) 272 | @file = file["tempfile"] || file[:tempfile] 273 | @original_filename = file["filename"] || file[:filename] 274 | @content_type = file["content_type"] || file[:content_type] 275 | else 276 | @file = file 277 | @original_filename = nil 278 | @content_type = nil 279 | end 280 | end 281 | 282 | # create the directory if it doesn't exist 283 | def mkdir!(path, directory_permissions) 284 | options = {} 285 | options[:mode] = directory_permissions if directory_permissions 286 | FileUtils.mkdir_p(File.dirname(path), options) unless File.exists?(File.dirname(path)) 287 | end 288 | 289 | def chmod!(path, permissions) 290 | File.chmod(permissions, path) if permissions 291 | end 292 | 293 | # Sanitize the filename, to prevent hacking 294 | def sanitize(name) 295 | name = name.gsub("\\", "/") # work-around for IE 296 | name = File.basename(name) 297 | name = name.gsub(sanitize_regexp,"_") 298 | name = "_#{name}" if name =~ /\A\.+\z/ 299 | name = "unnamed" if name.size == 0 300 | return name.mb_chars.to_s 301 | end 302 | 303 | def split_extension(filename) 304 | # regular expressions to try for identifying extensions 305 | extension_matchers = [ 306 | /\A(.+)\.(tar\.([glx]?z|bz2))\z/, # matches "something.tar.gz" 307 | /\A(.+)\.([^\.]+)\z/ # matches "something.jpg" 308 | ] 309 | 310 | extension_matchers.each do |regexp| 311 | if filename =~ regexp 312 | return $1, $2 313 | end 314 | end 315 | return filename, "" # In case we weren't able to split the extension 316 | end 317 | 318 | end # SanitizedFile 319 | end # CarrierWave 320 | -------------------------------------------------------------------------------- /lib/carrierwave/processing/mini_magick.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | 5 | ## 6 | # This module simplifies manipulation with MiniMagick by providing a set 7 | # of convenient helper methods. If you want to use them, you'll need to 8 | # require this file: 9 | # 10 | # require 'carrierwave/processing/mini_magick' 11 | # 12 | # And then include it in your uploader: 13 | # 14 | # class MyUploader < CarrierWave::Uploader::Base 15 | # include CarrierWave::MiniMagick 16 | # end 17 | # 18 | # You can now use the provided helpers: 19 | # 20 | # class MyUploader < CarrierWave::Uploader::Base 21 | # include CarrierWave::MiniMagick 22 | # 23 | # process :resize_to_fit => [200, 200] 24 | # end 25 | # 26 | # Or create your own helpers with the powerful manipulate! method. Check 27 | # out the ImageMagick docs at http://www.imagemagick.org/script/command-line-options.php for more 28 | # info 29 | # 30 | # class MyUploader < CarrierWave::Uploader::Base 31 | # include CarrierWave::MiniMagick 32 | # 33 | # process :radial_blur => 10 34 | # 35 | # def radial_blur(amount) 36 | # manipulate! do |img| 37 | # img.radial_blur(amount) 38 | # img = yield(img) if block_given? 39 | # img 40 | # end 41 | # end 42 | # 43 | # === Note 44 | # 45 | # MiniMagick is a mini replacement for RMagick that uses the command line 46 | # tool "mogrify" for image manipulation. 47 | # 48 | # You can find more information here: 49 | # 50 | # http://mini_magick.rubyforge.org/ 51 | # and 52 | # https://github.com/minimagic/minimagick/ 53 | # 54 | # 55 | module MiniMagick 56 | extend ActiveSupport::Concern 57 | 58 | included do 59 | begin 60 | require "mini_magick" 61 | rescue LoadError => e 62 | e.message << " (You may need to install the mini_magick gem)" 63 | raise e 64 | end 65 | end 66 | 67 | module ClassMethods 68 | def convert(format) 69 | process :convert => format 70 | end 71 | 72 | def resize_to_limit(width, height) 73 | process :resize_to_limit => [width, height] 74 | end 75 | 76 | def resize_to_fit(width, height) 77 | process :resize_to_fit => [width, height] 78 | end 79 | 80 | def resize_to_fill(width, height, gravity='Center') 81 | process :resize_to_fill => [width, height, gravity] 82 | end 83 | 84 | def resize_and_pad(width, height, background=:transparent, gravity=::Magick::CenterGravity) 85 | process :resize_and_pad => [width, height, background, gravity] 86 | end 87 | end 88 | 89 | ## 90 | # Changes the image encoding format to the given format 91 | # 92 | # See http://www.imagemagick.org/script/command-line-options.php#format 93 | # 94 | # === Parameters 95 | # 96 | # [format (#to_s)] an abreviation of the format 97 | # 98 | # === Yields 99 | # 100 | # [MiniMagick::Image] additional manipulations to perform 101 | # 102 | # === Examples 103 | # 104 | # image.convert(:png) 105 | # 106 | def convert(format) 107 | manipulate! do |img| 108 | img.format(format.to_s.downcase) 109 | img = yield(img) if block_given? 110 | img 111 | end 112 | end 113 | 114 | ## 115 | # Resize the image to fit within the specified dimensions while retaining 116 | # the original aspect ratio. Will only resize the image if it is larger than the 117 | # specified dimensions. The resulting image may be shorter or narrower than specified 118 | # in the smaller dimension but will not be larger than the specified values. 119 | # 120 | # === Parameters 121 | # 122 | # [width (Integer)] the width to scale the image to 123 | # [height (Integer)] the height to scale the image to 124 | # 125 | # === Yields 126 | # 127 | # [MiniMagick::Image] additional manipulations to perform 128 | # 129 | def resize_to_limit(width, height) 130 | manipulate! do |img| 131 | img.resize "#{width}x#{height}>" 132 | img = yield(img) if block_given? 133 | img 134 | end 135 | end 136 | 137 | ## 138 | # Resize the image to fit within the specified dimensions while retaining 139 | # the original aspect ratio. The image may be shorter or narrower than 140 | # specified in the smaller dimension but will not be larger than the specified values. 141 | # 142 | # === Parameters 143 | # 144 | # [width (Integer)] the width to scale the image to 145 | # [height (Integer)] the height to scale the image to 146 | # 147 | # === Yields 148 | # 149 | # [MiniMagick::Image] additional manipulations to perform 150 | # 151 | def resize_to_fit(width, height) 152 | manipulate! do |img| 153 | img.resize "#{width}x#{height}" 154 | img = yield(img) if block_given? 155 | img 156 | end 157 | end 158 | 159 | ## 160 | # Resize the image to fit within the specified dimensions while retaining 161 | # the aspect ratio of the original image. If necessary, crop the image in the 162 | # larger dimension. 163 | # 164 | # === Parameters 165 | # 166 | # [width (Integer)] the width to scale the image to 167 | # [height (Integer)] the height to scale the image to 168 | # [gravity (String)] the current gravity suggestion (default: 'Center'; options: 'NorthWest', 'North', 'NorthEast', 'West', 'Center', 'East', 'SouthWest', 'South', 'SouthEast') 169 | # 170 | # === Yields 171 | # 172 | # [MiniMagick::Image] additional manipulations to perform 173 | # 174 | def resize_to_fill(width, height, gravity = 'Center') 175 | manipulate! do |img| 176 | cols, rows = img[:dimensions] 177 | img.combine_options do |cmd| 178 | if width != cols || height != rows 179 | scale_x = width/cols.to_f 180 | scale_y = height/rows.to_f 181 | if scale_x >= scale_y 182 | cols = (scale_x * (cols + 0.5)).round 183 | rows = (scale_x * (rows + 0.5)).round 184 | cmd.resize "#{cols}" 185 | else 186 | cols = (scale_y * (cols + 0.5)).round 187 | rows = (scale_y * (rows + 0.5)).round 188 | cmd.resize "x#{rows}" 189 | end 190 | end 191 | cmd.gravity gravity 192 | cmd.background "rgba(255,255,255,0.0)" 193 | cmd.extent "#{width}x#{height}" if cols != width || rows != height 194 | end 195 | img = yield(img) if block_given? 196 | img 197 | end 198 | end 199 | 200 | ## 201 | # Resize the image to fit within the specified dimensions while retaining 202 | # the original aspect ratio. If necessary, will pad the remaining area 203 | # with the given color, which defaults to transparent (for gif and png, 204 | # white for jpeg). 205 | # 206 | # See http://www.imagemagick.org/script/command-line-options.php#gravity 207 | # for gravity options. 208 | # 209 | # === Parameters 210 | # 211 | # [width (Integer)] the width to scale the image to 212 | # [height (Integer)] the height to scale the image to 213 | # [background (String, :transparent)] the color of the background as a hexcode, like "#ff45de" 214 | # [gravity (String)] how to position the image 215 | # 216 | # === Yields 217 | # 218 | # [MiniMagick::Image] additional manipulations to perform 219 | # 220 | def resize_and_pad(width, height, background=:transparent, gravity='Center') 221 | manipulate! do |img| 222 | img.combine_options do |cmd| 223 | cmd.thumbnail "#{width}x#{height}>" 224 | if background == :transparent 225 | cmd.background "rgba(255, 255, 255, 0.0)" 226 | else 227 | cmd.background background 228 | end 229 | cmd.gravity gravity 230 | cmd.extent "#{width}x#{height}" 231 | end 232 | img = yield(img) if block_given? 233 | img 234 | end 235 | end 236 | 237 | ## 238 | # Manipulate the image with MiniMagick. This method will load up an image 239 | # and then pass each of its frames to the supplied block. It will then 240 | # save the image to disk. 241 | # 242 | # === Gotcha 243 | # 244 | # This method assumes that the object responds to +current_path+. 245 | # Any class that this module is mixed into must have a +current_path+ method. 246 | # CarrierWave::Uploader does, so you won't need to worry about this in 247 | # most cases. 248 | # 249 | # === Yields 250 | # 251 | # [MiniMagick::Image] manipulations to perform 252 | # 253 | # === Raises 254 | # 255 | # [CarrierWave::ProcessingError] if manipulation failed. 256 | # 257 | def manipulate! 258 | cache_stored_file! if !cached? 259 | image = ::MiniMagick::Image.open(current_path) 260 | image = yield(image) 261 | image.write(current_path) 262 | ::MiniMagick::Image.open(current_path) 263 | rescue ::MiniMagick::Error, ::MiniMagick::Invalid => e 264 | raise CarrierWave::ProcessingError, I18n.translate(:"errors.messages.mini_magick_processing_error", :e => e) 265 | end 266 | 267 | end # MiniMagick 268 | end # CarrierWave 269 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/versions.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Versions 6 | extend ActiveSupport::Concern 7 | 8 | include CarrierWave::Uploader::Callbacks 9 | 10 | included do 11 | class_attribute :versions, :version_names, :instance_reader => false, :instance_writer => false 12 | 13 | self.versions = {} 14 | self.version_names = [] 15 | 16 | attr_accessor :parent_cache_id 17 | 18 | after :cache, :assign_parent_cache_id 19 | after :cache, :cache_versions! 20 | after :store, :store_versions! 21 | after :remove, :remove_versions! 22 | after :retrieve_from_cache, :retrieve_versions_from_cache! 23 | after :retrieve_from_store, :retrieve_versions_from_store! 24 | end 25 | 26 | module ClassMethods 27 | 28 | ## 29 | # Adds a new version to this uploader 30 | # 31 | # === Parameters 32 | # 33 | # [name (#to_sym)] name of the version 34 | # [options (Hash)] optional options hash 35 | # [&block (Proc)] a block to eval on this version of the uploader 36 | # 37 | # === Examples 38 | # 39 | # class MyUploader < CarrierWave::Uploader::Base 40 | # 41 | # version :thumb do 42 | # process :scale => [200, 200] 43 | # end 44 | # 45 | # version :preview, :if => :image? do 46 | # process :scale => [200, 200] 47 | # end 48 | # 49 | # end 50 | # 51 | def version(name, options = {}, &block) 52 | name = name.to_sym 53 | unless versions[name] 54 | uploader = Class.new(self) 55 | uploader.versions = {} 56 | 57 | # Define the enable_processing method for versions so they get the 58 | # value from the parent class unless explicitly overwritten 59 | uploader.class_eval <<-RUBY, __FILE__, __LINE__ + 1 60 | def self.enable_processing(value=nil) 61 | self.enable_processing = value if value 62 | if !@enable_processing.nil? 63 | @enable_processing 64 | else 65 | superclass.enable_processing 66 | end 67 | end 68 | RUBY 69 | 70 | # Add the current version hash to class attribute :versions 71 | current_version = {} 72 | current_version[name] = { 73 | :uploader => uploader, 74 | :options => options 75 | } 76 | self.versions = versions.merge(current_version) 77 | 78 | versions[name][:uploader].version_names += [name] 79 | 80 | class_eval <<-RUBY 81 | def #{name} 82 | versions[:#{name}] 83 | end 84 | RUBY 85 | # as the processors get the output from the previous processors as their 86 | # input we must not stack the processors here 87 | versions[name][:uploader].processors = versions[name][:uploader].processors.dup 88 | versions[name][:uploader].processors.clear 89 | end 90 | versions[name][:uploader].class_eval(&block) if block 91 | versions[name] 92 | end 93 | 94 | def recursively_apply_block_to_versions(&block) 95 | versions.each do |name, version| 96 | version[:uploader].class_eval(&block) 97 | version[:uploader].recursively_apply_block_to_versions(&block) 98 | end 99 | end 100 | end # ClassMethods 101 | 102 | ## 103 | # Returns a hash mapping the name of each version of the uploader to an instance of it 104 | # 105 | # === Returns 106 | # 107 | # [Hash{Symbol => CarrierWave::Uploader}] a list of uploader instances 108 | # 109 | def versions 110 | return @versions if @versions 111 | @versions = {} 112 | self.class.versions.each do |name, version| 113 | @versions[name] = version[:uploader].new(model, mounted_as) 114 | end 115 | @versions 116 | end 117 | 118 | ## 119 | # === Returns 120 | # 121 | # [String] the name of this version of the uploader 122 | # 123 | def version_name 124 | self.class.version_names.join('_').to_sym unless self.class.version_names.blank? 125 | end 126 | 127 | ## 128 | # When given a version name as a parameter, will return the url for that version 129 | # This also works with nested versions. 130 | # When given a query hash as a parameter, will return the url with signature that contains query params 131 | # Query hash only works with AWS (S3 storage). 132 | # 133 | # === Example 134 | # 135 | # my_uploader.url # => /path/to/my/uploader.gif 136 | # my_uploader.url(:thumb) # => /path/to/my/thumb_uploader.gif 137 | # my_uploader.url(:thumb, :small) # => /path/to/my/thumb_small_uploader.gif 138 | # my_uploader.url(:query => {"response-content-disposition" => "attachment"}) 139 | # my_uploader.url(:version, :sub_version, :query => {"response-content-disposition" => "attachment"}) 140 | # 141 | # === Parameters 142 | # 143 | # [*args (Symbol)] any number of versions 144 | # OR/AND 145 | # [Hash] query params 146 | # 147 | # === Returns 148 | # 149 | # [String] the location where this file is accessible via a url 150 | # 151 | def url(*args) 152 | if (version = args.first) && version.respond_to?(:to_sym) 153 | raise ArgumentError, "Version #{version} doesn't exist!" if versions[version.to_sym].nil? 154 | # recursively proxy to version 155 | versions[version.to_sym].url(*args[1..-1]) 156 | elsif args.first 157 | super(args.first) 158 | else 159 | super 160 | end 161 | end 162 | 163 | ## 164 | # Recreate versions and reprocess them. This can be used to recreate 165 | # versions if their parameters somehow have changed. 166 | # 167 | def recreate_versions!(*versions) 168 | # Some files could possibly not be stored on the local disk. This 169 | # doesn't play nicely with processing. Make sure that we're only 170 | # processing a cached file 171 | # 172 | # The call to store! will trigger the necessary callbacks to both 173 | # process this version and all sub-versions 174 | if versions.any? 175 | file = sanitized_file if !cached? 176 | store_versions!(file, versions) 177 | else 178 | cache! if !cached? 179 | store! 180 | end 181 | end 182 | 183 | private 184 | def assign_parent_cache_id(file) 185 | active_versions.each do |name, uploader| 186 | uploader.parent_cache_id = @cache_id 187 | end 188 | end 189 | 190 | def active_versions 191 | versions.select do |name, uploader| 192 | condition = self.class.versions[name][:options][:if] 193 | if(condition) 194 | if(condition.respond_to?(:call)) 195 | condition.call(self, :version => name, :file => file) 196 | else 197 | send(condition, file) 198 | end 199 | else 200 | true 201 | end 202 | end 203 | end 204 | 205 | def full_filename(for_file) 206 | [version_name, super(for_file)].compact.join('_') 207 | end 208 | 209 | def full_original_filename 210 | [version_name, super].compact.join('_') 211 | end 212 | 213 | def cache_versions!(new_file) 214 | # We might have processed the new_file argument after the callbacks were 215 | # initialized, so get the actual file based off of the current state of 216 | # our file 217 | processed_parent = SanitizedFile.new :tempfile => self.file, 218 | :filename => new_file.original_filename 219 | 220 | active_versions.each do |name, v| 221 | next if v.cached? 222 | 223 | v.send(:cache_id=, cache_id) 224 | # If option :from_version is present, create cache using cached file from 225 | # version indicated 226 | if self.class.versions[name][:options] && self.class.versions[name][:options][:from_version] 227 | # Maybe the reference version has not been cached yet 228 | unless versions[self.class.versions[name][:options][:from_version]].cached? 229 | versions[self.class.versions[name][:options][:from_version]].cache!(processed_parent) 230 | end 231 | processed_version = SanitizedFile.new :tempfile => versions[self.class.versions[name][:options][:from_version]], 232 | :filename => new_file.original_filename 233 | v.cache!(processed_version) 234 | else 235 | v.cache!(processed_parent) 236 | end 237 | end 238 | end 239 | 240 | def store_versions!(new_file, versions=nil) 241 | if versions 242 | versions.each { |v| Hash[active_versions][v].store!(new_file) } 243 | else 244 | active_versions.each { |name, v| v.store!(new_file) } 245 | end 246 | end 247 | 248 | def remove_versions! 249 | versions.each { |name, v| v.remove! } 250 | end 251 | 252 | def retrieve_versions_from_cache!(cache_name) 253 | versions.each { |name, v| v.retrieve_from_cache!(cache_name) } 254 | end 255 | 256 | def retrieve_versions_from_store!(identifier) 257 | versions.each { |name, v| v.retrieve_from_store!(identifier) } 258 | end 259 | 260 | end # Versions 261 | end # Uploader 262 | end # CarrierWave 263 | -------------------------------------------------------------------------------- /lib/carrierwave/test/matchers.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Test 5 | 6 | ## 7 | # These are some matchers that can be used in RSpec specs, to simplify the testing 8 | # of uploaders. 9 | # 10 | module Matchers 11 | 12 | class BeIdenticalTo # :nodoc: 13 | def initialize(expected) 14 | @expected = expected 15 | end 16 | 17 | def matches?(actual) 18 | @actual = actual 19 | FileUtils.identical?(@actual, @expected) 20 | end 21 | 22 | def failure_message 23 | "expected #{@actual.inspect} to be identical to #{@expected.inspect}" 24 | end 25 | 26 | def negative_failure_message 27 | "expected #{@actual.inspect} to not be identical to #{@expected.inspect}" 28 | end 29 | 30 | def description 31 | "be identical to #{@expected.inspect}" 32 | end 33 | end 34 | 35 | def be_identical_to(expected) 36 | BeIdenticalTo.new(expected) 37 | end 38 | 39 | class HavePermissions # :nodoc: 40 | def initialize(expected) 41 | @expected = expected 42 | end 43 | 44 | def matches?(actual) 45 | @actual = actual 46 | # Satisfy expectation here. Return false or raise an error if it's not met. 47 | (File.stat(@actual.path).mode & 0777) == @expected 48 | end 49 | 50 | def failure_message 51 | "expected #{@actual.current_path.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0777).to_s(8)}" 52 | end 53 | 54 | def negative_failure_message 55 | "expected #{@actual.current_path.inspect} not to have permissions #{@expected.to_s(8)}, but it did" 56 | end 57 | 58 | def description 59 | "have permissions #{@expected.to_s(8)}" 60 | end 61 | end 62 | 63 | def have_permissions(expected) 64 | HavePermissions.new(expected) 65 | end 66 | 67 | class HaveDirectoryPermissions # :nodoc: 68 | def initialize(expected) 69 | @expected = expected 70 | end 71 | 72 | def matches?(actual) 73 | @actual = actual 74 | # Satisfy expectation here. Return false or raise an error if it's not met. 75 | (File.stat(File.dirname @actual.path).mode & 0777) == @expected 76 | end 77 | 78 | def failure_message 79 | "expected #{File.dirname @actual.current_path.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0777).to_s(8)}" 80 | end 81 | 82 | def negative_failure_message 83 | "expected #{File.dirname @actual.current_path.inspect} not to have permissions #{@expected.to_s(8)}, but it did" 84 | end 85 | 86 | def description 87 | "have permissions #{@expected.to_s(8)}" 88 | end 89 | end 90 | 91 | def have_directory_permissions(expected) 92 | HaveDirectoryPermissions.new(expected) 93 | end 94 | 95 | class BeNoLargerThan # :nodoc: 96 | def initialize(width, height) 97 | @width, @height = width, height 98 | end 99 | 100 | def matches?(actual) 101 | @actual = actual 102 | # Satisfy expectation here. Return false or raise an error if it's not met. 103 | image = ImageLoader.load_image(@actual.current_path) 104 | @actual_width = image.width 105 | @actual_height = image.height 106 | @actual_width <= @width && @actual_height <= @height 107 | end 108 | 109 | def failure_message 110 | "expected #{@actual.current_path.inspect} to be no larger than #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}." 111 | end 112 | 113 | def negative_failure_message 114 | "expected #{@actual.current_path.inspect} to be larger than #{@width} by #{@height}, but it wasn't." 115 | end 116 | 117 | def description 118 | "be no larger than #{@width} by #{@height}" 119 | end 120 | end 121 | 122 | def be_no_larger_than(width, height) 123 | BeNoLargerThan.new(width, height) 124 | end 125 | 126 | class HaveDimensions # :nodoc: 127 | def initialize(width, height) 128 | @width, @height = width, height 129 | end 130 | 131 | def matches?(actual) 132 | @actual = actual 133 | # Satisfy expectation here. Return false or raise an error if it's not met. 134 | image = ImageLoader.load_image(@actual.current_path) 135 | @actual_width = image.width 136 | @actual_height = image.height 137 | @actual_width == @width && @actual_height == @height 138 | end 139 | 140 | def failure_message 141 | "expected #{@actual.current_path.inspect} to have an exact size of #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}." 142 | end 143 | 144 | def negative_failure_message 145 | "expected #{@actual.current_path.inspect} not to have an exact size of #{@width} by #{@height}, but it did." 146 | end 147 | 148 | def description 149 | "have an exact size of #{@width} by #{@height}" 150 | end 151 | end 152 | 153 | def have_dimensions(width, height) 154 | HaveDimensions.new(width, height) 155 | end 156 | 157 | class HaveHeight # :nodoc: 158 | def initialize(height) 159 | @height = height 160 | end 161 | 162 | def matches?(actual) 163 | @actual = actual 164 | # Satisfy expectation here. Return false or raise an error if it's not met. 165 | image = ImageLoader.load_image(@actual.current_path) 166 | @actual_height = image.height 167 | @actual_height == @height 168 | end 169 | 170 | def failure_message 171 | "expected #{@actual.current_path.inspect} to have an exact size of #{@height}, but it was #{@actual_height}." 172 | end 173 | 174 | def negative_failure_message 175 | "expected #{@actual.current_path.inspect} not to have an exact size of #{@height}, but it did." 176 | end 177 | 178 | def description 179 | "have an exact height of #{@height}" 180 | end 181 | end 182 | 183 | def have_height(height) 184 | HaveHeight.new(height) 185 | end 186 | 187 | class HaveWidth # :nodoc: 188 | def initialize(width) 189 | @width = width 190 | end 191 | 192 | def matches?(actual) 193 | @actual = actual 194 | # Satisfy expectation here. Return false or raise an error if it's not met. 195 | image = ImageLoader.load_image(@actual.current_path) 196 | @actual_width = image.width 197 | @actual_width == @width 198 | end 199 | 200 | def failure_message 201 | "expected #{@actual.current_path.inspect} to have an exact size of #{@width}, but it was #{@actual_width}." 202 | end 203 | 204 | def negative_failure_message 205 | "expected #{@actual.current_path.inspect} not to have an exact size of #{@width}, but it did." 206 | end 207 | 208 | def description 209 | "have an exact width of #{@width}" 210 | end 211 | end 212 | 213 | def have_width(width) 214 | HaveWidth.new(width) 215 | end 216 | 217 | class BeNoWiderThan # :nodoc: 218 | def initialize(width) 219 | @width = width 220 | end 221 | 222 | def matches?(actual) 223 | @actual = actual 224 | # Satisfy expectation here. Return false or raise an error if it's not met. 225 | image = ImageLoader.load_image(@actual.current_path) 226 | @actual_width = image.width 227 | @actual_width <= @width 228 | end 229 | 230 | def failure_message 231 | "expected #{@actual.current_path.inspect} to be no wider than #{@width}, but it was #{@actual_width}." 232 | end 233 | 234 | def negative_failure_message 235 | "expected #{@actual.current_path.inspect} not to be wider than #{@width}, but it is." 236 | end 237 | 238 | def description 239 | "have a width less than or equal to #{@width}" 240 | end 241 | end 242 | 243 | def be_no_wider_than(width) 244 | BeNoWiderThan.new(width) 245 | end 246 | 247 | class BeNoTallerThan # :nodoc: 248 | def initialize(height) 249 | @height = height 250 | end 251 | 252 | def matches?(actual) 253 | @actual = actual 254 | # Satisfy expectation here. Return false or raise an error if it's not met. 255 | image = ImageLoader.load_image(@actual.current_path) 256 | @actual_height = image.height 257 | @actual_height <= @height 258 | end 259 | 260 | def failure_message 261 | "expected #{@actual.current_path.inspect} to be no taller than #{@height}, but it was #{@actual_height}." 262 | end 263 | 264 | def negative_failure_message 265 | "expected #{@actual.current_path.inspect} not to be taller than #{@height}, but it is." 266 | end 267 | 268 | def description 269 | "have a height less than or equal to #{@height}" 270 | end 271 | end 272 | 273 | def be_no_taller_than(height) 274 | BeNoTallerThan.new(height) 275 | end 276 | 277 | class ImageLoader # :nodoc: 278 | def self.load_image(filename) 279 | if defined? ::MiniMagick 280 | MiniMagickWrapper.new(filename) 281 | else 282 | unless defined? ::Magick 283 | begin 284 | require 'rmagick' 285 | rescue LoadError 286 | require 'RMagick' 287 | rescue LoadError 288 | puts "WARNING: Failed to require rmagick, image processing may fail!" 289 | end 290 | end 291 | MagickWrapper.new(filename) 292 | end 293 | end 294 | end 295 | 296 | class MagickWrapper # :nodoc: 297 | attr_reader :image 298 | def width 299 | image.columns 300 | end 301 | 302 | def height 303 | image.rows 304 | end 305 | 306 | def initialize(filename) 307 | @image = ::Magick::Image.read(filename).first 308 | end 309 | end 310 | 311 | class MiniMagickWrapper # :nodoc: 312 | attr_reader :image 313 | def width 314 | image[:width] 315 | end 316 | 317 | def height 318 | image[:height] 319 | end 320 | 321 | def initialize(filename) 322 | @image = ::MiniMagick::Image.open(filename) 323 | end 324 | end 325 | 326 | end # Matchers 327 | end # Test 328 | end # CarrierWave 329 | 330 | -------------------------------------------------------------------------------- /spec/uploader/cache_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CarrierWave::Uploader do 6 | 7 | before do 8 | FileUtils.rm_rf(public_path) 9 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 10 | @uploader = @uploader_class.new 11 | end 12 | 13 | after do 14 | FileUtils.rm_rf(public_path) 15 | end 16 | 17 | describe '.clean_cached_files!' do 18 | before do 19 | @cache_dir = File.expand_path(@uploader_class.cache_dir, CarrierWave.root) 20 | FileUtils.mkdir_p File.expand_path('20071201-1234-234-2213', @cache_dir) 21 | FileUtils.mkdir_p File.expand_path('20071203-1234-234-2213', @cache_dir) 22 | FileUtils.mkdir_p File.expand_path('20071205-1234-234-2213', @cache_dir) 23 | end 24 | 25 | after { FileUtils.rm_rf(@cache_dir) } 26 | 27 | it "should clear all files older than, by defaul, 24 hours in the default cache directory" do 28 | Timecop.freeze(Time.utc(2007, 12, 6, 10, 12)) do 29 | @uploader_class.clean_cached_files! 30 | end 31 | Dir.glob("#{@cache_dir}/*").size.should == 1 32 | end 33 | 34 | it "should permit to set since how many seconds delete the cached files" do 35 | Timecop.freeze(Time.utc(2007, 12, 6, 10, 12)) do 36 | @uploader_class.clean_cached_files!(60*60*24*4) 37 | end 38 | Dir.glob("#{@cache_dir}/*").should have(2).element 39 | end 40 | 41 | it "should be aliased on the CarrierWave module" do 42 | Timecop.freeze(Time.utc(2007, 12, 6, 10, 12)) do 43 | CarrierWave.clean_cached_files! 44 | end 45 | Dir.glob("#{@cache_dir}/*").size.should == 1 46 | end 47 | end 48 | 49 | describe '#cache_dir' do 50 | it "should default to the config option" do 51 | @uploader.cache_dir.should == 'uploads/tmp' 52 | end 53 | end 54 | 55 | describe '#cache!' do 56 | 57 | before do 58 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 59 | end 60 | 61 | it "should cache a file" do 62 | @uploader.cache!(File.open(file_path('test.jpg'))) 63 | @uploader.file.should be_an_instance_of(CarrierWave::SanitizedFile) 64 | end 65 | 66 | it "should be cached" do 67 | @uploader.cache!(File.open(file_path('test.jpg'))) 68 | @uploader.should be_cached 69 | end 70 | 71 | it "should store the cache name" do 72 | @uploader.cache!(File.open(file_path('test.jpg'))) 73 | @uploader.cache_name.should == '20071201-1234-345-2255/test.jpg' 74 | end 75 | 76 | it "should set the filename to the file's sanitized filename" do 77 | @uploader.cache!(File.open(file_path('test.jpg'))) 78 | @uploader.filename.should == 'test.jpg' 79 | end 80 | 81 | it "should move it to the tmp dir" do 82 | @uploader.cache!(File.open(file_path('test.jpg'))) 83 | @uploader.file.path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 84 | @uploader.file.exists?.should be_true 85 | end 86 | 87 | it "should set the url" do 88 | @uploader.cache!(File.open(file_path('test.jpg'))) 89 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 90 | end 91 | 92 | it "should raise an error when trying to cache a string" do 93 | running { 94 | @uploader.cache!(file_path('test.jpg')) 95 | }.should raise_error(CarrierWave::FormNotMultipart) 96 | end 97 | 98 | it "should raise an error when trying to cache a pathname" do 99 | running { 100 | @uploader.cache!(Pathname.new(file_path('test.jpg'))) 101 | }.should raise_error(CarrierWave::FormNotMultipart) 102 | end 103 | 104 | it "should do nothing when trying to cache an empty file" do 105 | @uploader.cache!(nil) 106 | end 107 | 108 | it "should set permissions if options are given" do 109 | @uploader_class.permissions = 0777 110 | 111 | @uploader.cache!(File.open(file_path('test.jpg'))) 112 | @uploader.should have_permissions(0777) 113 | end 114 | 115 | it "should set directory permissions if options are given" do 116 | @uploader_class.directory_permissions = 0777 117 | 118 | @uploader.cache!(File.open(file_path('test.jpg'))) 119 | @uploader.should have_directory_permissions(0777) 120 | end 121 | 122 | describe "with ensuring multipart form deactivated" do 123 | 124 | before do 125 | CarrierWave.configure do |config| 126 | config.ensure_multipart_form = false 127 | end 128 | end 129 | 130 | it "should not raise an error when trying to cache a string" do 131 | running { 132 | @uploader.cache!(file_path('test.jpg')) 133 | }.should_not raise_error(CarrierWave::FormNotMultipart) 134 | end 135 | 136 | it "should raise an error when trying to cache a pathname and " do 137 | running { 138 | @uploader.cache!(Pathname.new(file_path('test.jpg'))) 139 | }.should_not raise_error(CarrierWave::FormNotMultipart) 140 | end 141 | 142 | end 143 | 144 | describe "with the move_to_cache option" do 145 | 146 | before do 147 | ## make a copy 148 | file = file_path('test.jpg') 149 | tmpfile = file_path("test_move.jpeg") 150 | FileUtils.rm_f(tmpfile) 151 | FileUtils.cp(file, File.join(File.dirname(file), "test_move.jpeg")) 152 | @tmpfile = File.open(tmpfile) 153 | 154 | ## stub 155 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 156 | 157 | @cached_path = public_path('uploads/tmp/20071201-1234-345-2255/test_move.jpeg') 158 | @uploader_class.permissions = 0777 159 | @uploader_class.directory_permissions = 0777 160 | end 161 | 162 | after do 163 | FileUtils.rm_f(@tmpfile.path) 164 | end 165 | 166 | context "set to true" do 167 | before do 168 | @uploader_class.move_to_cache = true 169 | end 170 | 171 | it "should move it from the upload dir to the tmp dir" do 172 | original_path = @tmpfile.path 173 | @uploader.cache!(@tmpfile) 174 | @uploader.file.path.should == @cached_path 175 | File.exist?(@cached_path).should be_true 176 | File.exist?(original_path).should be_false 177 | end 178 | 179 | it "should use move_to() during cache!()" do 180 | CarrierWave::SanitizedFile.any_instance.should_receive(:move_to).with(@cached_path, 0777, 0777) 181 | @uploader.cache!(@tmpfile) 182 | end 183 | 184 | it "should not use copy_to() during cache!()" do 185 | CarrierWave::SanitizedFile.any_instance.should_not_receive(:copy_to) 186 | @uploader.cache!(@tmpfile) 187 | end 188 | end 189 | 190 | context "set to false" do 191 | before do 192 | @uploader_class.move_to_cache = false 193 | end 194 | 195 | it "should copy it from the upload dir to the tmp dir" do 196 | original_path = @tmpfile.path 197 | @uploader.cache!(@tmpfile) 198 | @uploader.file.path.should == @cached_path 199 | File.exist?(@cached_path).should be_true 200 | File.exist?(original_path).should be_true 201 | end 202 | 203 | it "should use copy_to() during cache!()" do 204 | CarrierWave::SanitizedFile.any_instance.should_receive(:copy_to).with(@cached_path, 0777, 0777) 205 | @uploader.cache!(@tmpfile) 206 | end 207 | 208 | it "should not use move_to() during cache!()" do 209 | CarrierWave::SanitizedFile.any_instance.should_not_receive(:move_to) 210 | @uploader.cache!(@tmpfile) 211 | end 212 | end 213 | 214 | end 215 | end 216 | 217 | describe '#retrieve_from_cache!' do 218 | it "should cache a file" do 219 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 220 | @uploader.file.should be_an_instance_of(CarrierWave::SanitizedFile) 221 | end 222 | 223 | it "should be cached" do 224 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 225 | @uploader.should be_cached 226 | end 227 | 228 | it "should set the path to the tmp dir" do 229 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 230 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpeg') 231 | end 232 | 233 | it "should overwrite a file that has already been cached" do 234 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 235 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/bork.txt') 236 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/bork.txt') 237 | end 238 | 239 | it "should store the cache_name" do 240 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 241 | @uploader.cache_name.should == '20071201-1234-345-2255/test.jpeg' 242 | end 243 | 244 | it "should store the filename" do 245 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 246 | @uploader.filename.should == 'test.jpeg' 247 | end 248 | 249 | it "should set the url" do 250 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 251 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpeg' 252 | end 253 | 254 | it "should raise an error when the cache_id has an invalid format" do 255 | running { 256 | @uploader.retrieve_from_cache!('12345/test.jpeg') 257 | }.should raise_error(CarrierWave::InvalidParameter) 258 | 259 | @uploader.file.should be_nil 260 | @uploader.filename.should be_nil 261 | @uploader.cache_name.should be_nil 262 | end 263 | 264 | it "should raise an error when the original_filename contains invalid characters" do 265 | running { 266 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/te/st.jpeg') 267 | }.should raise_error(CarrierWave::InvalidParameter) 268 | running { 269 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/te??%st.jpeg') 270 | }.should raise_error(CarrierWave::InvalidParameter) 271 | 272 | @uploader.file.should be_nil 273 | @uploader.filename.should be_nil 274 | @uploader.cache_name.should be_nil 275 | end 276 | end 277 | 278 | describe 'with an overridden, reversing, filename' do 279 | before do 280 | @uploader_class.class_eval do 281 | def filename 282 | super.reverse unless super.blank? 283 | end 284 | end 285 | end 286 | 287 | describe '#cache!' do 288 | 289 | before do 290 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 291 | end 292 | 293 | it "should set the filename to the file's reversed filename" do 294 | @uploader.cache!(File.open(file_path('test.jpg'))) 295 | @uploader.filename.should == "gpj.tset" 296 | end 297 | 298 | it "should move it to the tmp dir with the filename unreversed" do 299 | @uploader.cache!(File.open(file_path('test.jpg'))) 300 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 301 | @uploader.file.exists?.should be_true 302 | end 303 | end 304 | 305 | describe '#retrieve_from_cache!' do 306 | it "should set the path to the tmp dir" do 307 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpg') 308 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 309 | end 310 | 311 | it "should set the filename to the reversed name of the file" do 312 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpg') 313 | @uploader.filename.should == "gpj.tset" 314 | end 315 | end 316 | end 317 | 318 | end 319 | --------------------------------------------------------------------------------