├── spec ├── fixtures │ ├── case.JPG │ ├── new.jpeg │ ├── old.jpeg │ ├── test.jpeg │ ├── test.jpg │ ├── Uppercase.jpg │ ├── landscape.jpg │ ├── portrait.jpg │ ├── bork.txt │ ├── new.txt │ ├── old.txt │ ├── bork.ttxt │ └── bork.txtt ├── storage │ ├── fog_spec.rb │ ├── cloudfiles_spec.rb │ ├── grid_fs_spec.rb │ └── fog_helper.rb ├── uploader │ ├── paths_spec.rb │ ├── callback_spec.rb │ ├── mountable_spec.rb │ ├── proxy_spec.rb │ ├── remove_spec.rb │ ├── default_url_spec.rb │ ├── extension_whitelist_spec.rb │ ├── configuration_spec.rb │ ├── processing_spec.rb │ ├── url_spec.rb │ ├── download_spec.rb │ └── cache_spec.rb ├── fog_credentials.rb ├── processing │ ├── mime_types_spec.rb │ ├── image_science_spec.rb │ ├── rmagick_spec.rb │ └── mini_magick_spec.rb ├── compatibility │ └── paperclip_spec.rb └── spec_helper.rb ├── features ├── fixtures │ ├── bork.txt │ └── monkey.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 ├── grid_fs_storage.feature ├── caching.feature ├── file_storage.feature ├── file_storage_overridden_filename.feature ├── file_storage_overridden_store_dir.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 │ ├── storage │ │ ├── right_s3.rb │ │ ├── abstract.rb │ │ ├── file.rb │ │ ├── grid_fs.rb │ │ ├── cloud_files.rb │ │ └── s3.rb │ ├── locale │ │ └── en.yml │ ├── uploader │ │ ├── default_url.rb │ │ ├── remove.rb │ │ ├── url.rb │ │ ├── callbacks.rb │ │ ├── mountable.rb │ │ ├── extension_whitelist.rb │ │ ├── proxy.rb │ │ ├── download.rb │ │ ├── processing.rb │ │ ├── store.rb │ │ ├── cache.rb │ │ ├── configuration.rb │ │ └── versions.rb │ ├── orm │ │ ├── activerecord.rb │ │ └── mongoid.rb │ ├── uploader.rb │ ├── processing │ │ ├── mime_types.rb │ │ ├── image_science.rb │ │ └── mini_magick.rb │ ├── validations │ │ └── active_model.rb │ ├── compatibility │ │ └── paperclip.rb │ ├── test │ │ └── matchers.rb │ └── sanitized_file.rb ├── generators │ ├── uploader_generator.rb │ └── templates │ │ └── uploader.rb └── carrierwave.rb ├── cucumber.yml ├── .gitignore ├── script ├── destroy ├── generate └── console ├── Rakefile └── 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 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /features/fixtures/monkey.txt: -------------------------------------------------------------------------------- 1 | this is another file -------------------------------------------------------------------------------- /lib/carrierwave/version.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | VERSION = "0.5.4" 3 | end 4 | -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --format pretty --no-source 2 | html: --format html --out features.html -------------------------------------------------------------------------------- /spec/fixtures/landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/carrierwave/master/spec/fixtures/landscape.jpg -------------------------------------------------------------------------------- /spec/fixtures/portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/carrierwave/master/spec/fixtures/portrait.jpg -------------------------------------------------------------------------------- /lib/carrierwave/storage/right_s3.rb: -------------------------------------------------------------------------------- 1 | raise "The right_aws library is no longer supported. Please install the 'fog' gem instead." -------------------------------------------------------------------------------- /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/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | carrierwave_processing_error: failed to be processed 5 | carrierwave_integrity_error: is not an allowed file type -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | @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 '#root' do 17 | it "should default to the config option" do 18 | @uploader.root.should == public_path 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # not sure why we need to do this 4 | require 'sqlite3/sqlite3_native' 5 | require 'sqlite3' 6 | 7 | require 'active_record' 8 | require 'carrierwave/mount' 9 | require 'carrierwave/orm/activerecord' 10 | 11 | # change this if sqlite is unavailable 12 | dbconfig = { 13 | :adapter => 'sqlite3', 14 | :database => ':memory:' 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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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!] 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!, :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!] 21 | end 22 | 23 | 24 | end 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/url.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Url 6 | 7 | ## 8 | # === Returns 9 | # 10 | # [String] the location where this file is accessible via a url 11 | # 12 | def url 13 | if file.respond_to?(:url) and not file.url.blank? 14 | file.url 15 | elsif current_path 16 | File.expand_path(current_path).gsub(File.expand_path(root), '') 17 | end 18 | end 19 | 20 | alias_method :to_s, :url 21 | 22 | ## 23 | # === Returns 24 | # 25 | # [String] A JSON serialization containing this uploader's URL(s) 26 | # 27 | def as_json(options = nil) 28 | h = { :url => url } 29 | h.merge Hash[versions.map { |name, version| [name, { :url => version.url }] }] 30 | end 31 | 32 | end # Url 33 | end # Uploader 34 | end # CarrierWave 35 | -------------------------------------------------------------------------------- /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/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 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 -------------------------------------------------------------------------------- /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 | # === Parameters 17 | # 18 | # [file (CarrierWave::SanitizedFile)] the file to store 19 | # 20 | # === Returns 21 | # 22 | # [CarrierWave::SanitizedFile] a sanitized file 23 | # 24 | def store!(file) 25 | path = ::File.expand_path(uploader.store_path, uploader.root) 26 | file.copy_to(path, uploader.permissions) 27 | end 28 | 29 | ## 30 | # Retrieve the file from its store path 31 | # 32 | # === Parameters 33 | # 34 | # [identifier (String)] the filename of the file 35 | # 36 | # === Returns 37 | # 38 | # [CarrierWave::SanitizedFile] a sanitized file 39 | # 40 | def retrieve!(identifier) 41 | path = ::File.expand_path(uploader.store_path(identifier), uploader.root) 42 | CarrierWave::SanitizedFile.new(path) 43 | end 44 | 45 | end # File 46 | end # Storage 47 | end # CarrierWave 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 27 | after_save :"store_#{column}!" 28 | before_save :"write_#{column}_identifier" 29 | after_destroy :"remove_#{column}!" 30 | before_update :"store_previous_model_for_#{column}" 31 | after_save :"remove_previously_stored_#{column}" 32 | 33 | class_eval <<-RUBY, __FILE__, __LINE__+1 34 | def #{column}=(new_file) 35 | column = _mounter(:#{column}).serialization_column 36 | send(:"\#{column}_will_change!") 37 | super 38 | end 39 | RUBY 40 | 41 | end 42 | 43 | end # ActiveRecord 44 | end # CarrierWave 45 | 46 | ActiveRecord::Base.extend CarrierWave::ActiveRecord 47 | -------------------------------------------------------------------------------- /lib/generators/templates/uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class <%= class_name %>Uploader < CarrierWave::Uploader::Base 4 | 5 | # Include RMagick or ImageScience support: 6 | # include CarrierWave::RMagick 7 | # include CarrierWave::ImageScience 8 | 9 | # Choose what kind of storage to use for this uploader: 10 | storage :file 11 | # storage :fog 12 | 13 | # Override the directory where uploaded files will be stored. 14 | # This is a sensible default for uploaders that are meant to be mounted: 15 | def store_dir 16 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 17 | end 18 | 19 | # Provide a default URL as a default if there hasn't been a file uploaded: 20 | # def default_url 21 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_') 22 | # end 23 | 24 | # Process files as they are uploaded: 25 | # process :scale => [200, 300] 26 | # 27 | # def scale(width, height) 28 | # # do something 29 | # end 30 | 31 | # Create different versions of your uploaded files: 32 | # version :thumb do 33 | # process :scale => [50, 50] 34 | # end 35 | 36 | # Add a white list of extensions which are allowed to be uploaded. 37 | # For images you might use something like this: 38 | # def extension_white_list 39 | # %w(jpg jpeg gif png) 40 | # end 41 | 42 | # Override the filename of the uploaded files: 43 | # Avoid using model.id or version_name here, see uploader/store.rb for details. 44 | # def filename 45 | # "something.jpg" if original_filename 46 | # end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /features/grid_fs_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 'grid_fs' 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 the contents of the file should be 'this is a file' 13 | 14 | Scenario: store two files in succession 15 | When I store the file 'fixtures/bork.txt' 16 | Then the contents of the file should be 'this is a file' 17 | When I store the file 'fixtures/monkey.txt' 18 | Then the contents of the file should be 'this is another file' 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 | 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 there should not be a file at 'public/uploads/bork.txt' 25 | When I store the file 26 | Then the contents of the file should be 'this is a file' 27 | 28 | Scenario: retrieving a file from cache then storing 29 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/20090212-2343-8336-0348/bork.txt' 30 | When I retrieve the cache name '20090212-2343-8336-0348/bork.txt' from the cache 31 | And I store the file 32 | Then the contents of the file should be 'this is a file' -------------------------------------------------------------------------------- /lib/carrierwave/uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CarrierWave 4 | 5 | ## 6 | # See CarrierWave::Uploader::Base 7 | # 8 | module Uploader 9 | 10 | ## 11 | # An uploader is a class that allows you to easily handle the caching and storage of 12 | # uploaded files. Please refer to the README for configuration options. 13 | # 14 | # Once you have an uploader you can use it in isolation: 15 | # 16 | # my_uploader = MyUploader.new 17 | # my_uploader.cache!(File.open(path_to_file)) 18 | # my_uploader.retrieve_from_store!('monkey.png') 19 | # 20 | # Alternatively, you can mount it on an ORM or other persistence layer, with 21 | # +CarrierWave::Mount#mount_uploader+. There are extensions for activerecord and datamapper 22 | # these are *very* simple (they are only a dozen lines of code), so adding your own should 23 | # be trivial. 24 | # 25 | class Base 26 | attr_reader :file 27 | 28 | include CarrierWave::Uploader::Callbacks 29 | include CarrierWave::Uploader::Proxy 30 | include CarrierWave::Uploader::Url 31 | include CarrierWave::Uploader::Mountable 32 | include CarrierWave::Uploader::Cache 33 | include CarrierWave::Uploader::Store 34 | include CarrierWave::Uploader::Download 35 | include CarrierWave::Uploader::Remove 36 | include CarrierWave::Uploader::ExtensionWhitelist 37 | include CarrierWave::Uploader::Processing 38 | include CarrierWave::Uploader::Versions 39 | include CarrierWave::Uploader::DefaultUrl 40 | include CarrierWave::Uploader::Configuration 41 | end # Base 42 | 43 | end # Uploader 44 | end # CarrierWave 45 | -------------------------------------------------------------------------------- /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.0"]) 26 | 27 | s.add_development_dependency "rails", ["~> 3.0"] 28 | s.add_development_dependency "rspec", ["~> 2.0"] 29 | s.add_development_dependency "excon" 30 | s.add_development_dependency "fog" 31 | s.add_development_dependency "cucumber" 32 | s.add_development_dependency "sqlite3" 33 | s.add_development_dependency "rmagick" 34 | s.add_development_dependency "RubyInline" 35 | s.add_development_dependency "image_science" 36 | s.add_development_dependency "mini_magick" 37 | s.add_development_dependency "bson_ext" 38 | s.add_development_dependency "mongoid" 39 | s.add_development_dependency "timecop" 40 | s.add_development_dependency "json" 41 | s.add_development_dependency "cloudfiles" 42 | s.add_development_dependency "sham_rack" 43 | end 44 | -------------------------------------------------------------------------------- /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 "sets content_type if content_type is generic" do 38 | @instance.file.content_type = 'application/octet-stream' 39 | @instance.file.should_receive(:content_type=).with('image/jpeg') 40 | @instance.set_content_type 41 | end 42 | 43 | it "sets content_type if override is true" do 44 | @instance.file.content_type = 'image/jpeg' 45 | @instance.file.should_receive(:content_type=).with('image/jpeg') 46 | @instance.set_content_type(true) 47 | end 48 | 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /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, "You are not allowed to upload #{new_file.extension.inspect} files, allowed types: #{extension_white_list.inspect}" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/carrierwave/processing/mime_types.rb: -------------------------------------------------------------------------------- 1 | require 'mime/types' 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 | module ClassMethods 30 | def set_content_type(override=false) 31 | process :set_content_type => override 32 | end 33 | end 34 | 35 | ## 36 | # Changes the file content_type using the mime-types gem 37 | # 38 | # === Parameters 39 | # 40 | # [override (Boolean)] whether or not to override the file's content_type 41 | # if it is already set and not a generic content-type, 42 | # false by default 43 | # 44 | def set_content_type(override=false) 45 | if override || file.content_type.blank? || file.content_type == 'application/octet-stream' 46 | new_content_type = ::MIME::Types.type_for(file.original_filename).first.to_s 47 | if file.respond_to?(:content_type=) 48 | file.content_type = new_content_type 49 | else 50 | file.set_instance_variable(:@content_type, new_content_type) 51 | end 52 | end 53 | rescue ::MIME::InvalidContentType => e 54 | raise CarrierWave::ProcessingError.new("Failed to process file with MIME::Types, maybe not valid content-type? Original Error: #{e}") 55 | end 56 | 57 | end # MimeTypes 58 | end # CarrierWave 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | end 40 | 41 | def method_missing(*args, &block) 42 | file.send(*args, &block) 43 | end 44 | end 45 | 46 | ## 47 | # Caches the file by downloading it from the given URL. 48 | # 49 | # === Parameters 50 | # 51 | # [url (String)] The URL where the remote file is stored 52 | # 53 | def download!(uri) 54 | unless uri.blank? 55 | processed_uri = process_uri(uri) 56 | file = RemoteFile.new(processed_uri) 57 | raise CarrierWave::DownloadError, "trying to download a file which is not served over HTTP" unless file.http? 58 | cache!(file) 59 | end 60 | end 61 | 62 | ## 63 | # Processes the given URL by parsing and escaping it. Public to allow overriding. 64 | # 65 | # === Parameters 66 | # 67 | # [url (String)] The URL where the remote file is stored 68 | # 69 | def process_uri(uri) 70 | URI.parse(URI.escape(URI.unescape(uri))) 71 | end 72 | 73 | end # Download 74 | end # Uploader 75 | end # CarrierWave 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/orm/mongoid.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'mongoid' 4 | require 'carrierwave/validations/active_model' 5 | 6 | module CarrierWave 7 | module Mongoid 8 | include CarrierWave::Mount 9 | ## 10 | # See +CarrierWave::Mount#mount_uploader+ for documentation 11 | # 12 | def mount_uploader(column, uploader=nil, options={}, &block) 13 | options[:mount_on] ||= "#{column}_filename" 14 | field options[:mount_on] 15 | 16 | super 17 | 18 | alias_method :read_uploader, :read_attribute 19 | alias_method :write_uploader, :write_attribute 20 | 21 | include CarrierWave::Validations::ActiveModel 22 | 23 | validates_integrity_of column if uploader_option(column.to_sym, :validate_integrity) 24 | validates_processing_of column if uploader_option(column.to_sym, :validate_processing) 25 | 26 | after_save :"store_#{column}!" 27 | before_save :"write_#{column}_identifier" 28 | after_destroy :"remove_#{column}!" 29 | before_update :"store_previous_model_for_#{column}" 30 | after_save :"remove_previously_stored_#{column}" 31 | 32 | class_eval <<-RUBY, __FILE__, __LINE__+1 33 | def #{column}=(new_file) 34 | column = _mounter(:#{column}).serialization_column 35 | 36 | # Note (Didier L.): equivalent of the _will_change! ActiveModel method 37 | begin 38 | value = __send__(column) 39 | value = value.duplicable? ? value.clone : value 40 | rescue TypeError, NoMethodError 41 | end 42 | setup_modifications 43 | 44 | super.tap do 45 | @modifications[column] = [value, __send__(column)] 46 | end 47 | end 48 | 49 | def #{column}_changed? 50 | column = _mounter(:#{column}).serialization_column 51 | send(:"\#{column}_changed?") 52 | end 53 | 54 | def find_previous_model_for_#{column} 55 | if self.embedded? 56 | self._parent.reload.send(self.metadata.key).find(to_key.first) 57 | else 58 | self.class.find(to_key.first) 59 | end 60 | end 61 | 62 | RUBY 63 | 64 | end 65 | end # Mongoid 66 | end # CarrierWave 67 | 68 | Mongoid::Document::ClassMethods.send(:include, CarrierWave::Mongoid) 69 | -------------------------------------------------------------------------------- /spec/processing/image_science_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | # Seems like ImageScience doesn't work on 1.9 6 | if RUBY_VERSION =~ /^1\.9/ 7 | puts "ImageScience doesn't work on Ruby 1.9, skipping" 8 | else 9 | describe CarrierWave::ImageScience do 10 | 11 | before do 12 | @klass = Class.new do 13 | include CarrierWave::ImageScience 14 | end 15 | @instance = @klass.new 16 | FileUtils.cp(file_path('landscape.jpg'), file_path('landscape_copy.jpg')) 17 | @instance.stub(:current_path).and_return(file_path('landscape_copy.jpg')) 18 | @instance.stub(:cached?).and_return true 19 | end 20 | 21 | after do 22 | FileUtils.rm(file_path('landscape_copy.jpg')) 23 | end 24 | 25 | describe '#resize_to_fill' do 26 | it "should resize the image to exactly the given dimensions" do 27 | @instance.resize_to_fill(200, 200) 28 | @instance.should have_dimensions(200, 200) 29 | end 30 | 31 | it "should scale up the image if it smaller than the given dimensions" do 32 | @instance.resize_to_fill(1000, 1000) 33 | @instance.should have_dimensions(1000, 1000) 34 | end 35 | 36 | it "should resize to a aspect ratio between 4:3 to 2:1 (width:height)" do 37 | @instance.resize_to_fill(400, 250) 38 | @instance.should have_dimensions(400, 250) 39 | end 40 | end 41 | 42 | describe '#resize_to_fit' do 43 | it "should resize the image to fit within the given dimensions" do 44 | @instance.resize_to_fit(200, 200) 45 | @instance.should have_dimensions(200, 150) 46 | end 47 | 48 | it "should scale up the image if it smaller than the given dimensions" do 49 | @instance.resize_to_fit(1000, 1000) 50 | @instance.should have_dimensions(1000, 750) 51 | end 52 | end 53 | 54 | describe '#resize_to_limit' do 55 | it "should resize the image to fit within the given dimensions" do 56 | @instance.resize_to_limit(200, 200) 57 | @instance.should have_dimensions(200, 150) 58 | end 59 | 60 | it "should not scale up the image if it smaller than the given dimensions" do 61 | @instance.resize_to_limit(1000, 1000) 62 | @instance.should have_dimensions(640, 480) 63 | end 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /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 | end 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/carrierwave/validations/active_model.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'active_model/validator' 4 | require 'active_support/concern' 5 | 6 | 7 | module CarrierWave 8 | 9 | # == Active Model Presence Validator 10 | module Validations 11 | module ActiveModel 12 | extend ActiveSupport::Concern 13 | 14 | class ProcessingValidator < ::ActiveModel::EachValidator 15 | 16 | def validate_each(record, attribute, value) 17 | if record.send("#{attribute}_processing_error") 18 | record.errors.add(attribute, :carrierwave_processing_error) 19 | end 20 | end 21 | end 22 | 23 | class IntegrityValidator < ::ActiveModel::EachValidator 24 | 25 | def validate_each(record, attribute, value) 26 | if record.send("#{attribute}_integrity_error") 27 | record.errors.add(attribute, :carrierwave_integrity_error) 28 | end 29 | end 30 | end 31 | 32 | module HelperMethods 33 | 34 | ## 35 | # Makes the record invalid if the file couldn't be uploaded due to an integrity error 36 | # 37 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 38 | # 39 | # === Note 40 | # 41 | # Set this key in your translations file for I18n: 42 | # 43 | # carrierwave: 44 | # errors: 45 | # integrity: 'Here be an error message' 46 | # 47 | def validates_integrity_of(*attr_names) 48 | validates_with IntegrityValidator, _merge_attributes(attr_names) 49 | end 50 | 51 | ## 52 | # Makes the record invalid if the file couldn't be processed (assuming the process failed 53 | # with a CarrierWave::ProcessingError) 54 | # 55 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 56 | # 57 | # === Note 58 | # 59 | # Set this key in your translations file for I18n: 60 | # 61 | # carrierwave: 62 | # errors: 63 | # processing: 'Here be an error message' 64 | # 65 | def validates_processing_of(*attr_names) 66 | validates_with ProcessingValidator, _merge_attributes(attr_names) 67 | end 68 | end 69 | 70 | included do 71 | extend HelperMethods 72 | include HelperMethods 73 | end 74 | end 75 | end 76 | end 77 | 78 | I18n.load_path << File.join(File.dirname(__FILE__), "..", "locale", 'en.yml') 79 | 80 | -------------------------------------------------------------------------------- /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/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 | end 52 | 53 | describe '#resize_to_fit' do 54 | it "should resize the image to fit within the given dimensions" do 55 | @instance.resize_to_fit(200, 200) 56 | @instance.should have_dimensions(200, 150) 57 | end 58 | 59 | it "should scale up the image if it smaller than the given dimensions" do 60 | @instance.resize_to_fit(1000, 1000) 61 | @instance.should have_dimensions(1000, 750) 62 | end 63 | end 64 | 65 | describe '#resize_to_limit' do 66 | it "should resize the image to fit within the given dimensions" do 67 | @instance.resize_to_limit(200, 200) 68 | @instance.should have_dimensions(200, 150) 69 | end 70 | 71 | it "should not scale up the image if it smaller than the given dimensions" do 72 | @instance.resize_to_limit(1000, 1000) 73 | @instance.should have_dimensions(640, 480) 74 | end 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /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 | Then /^the contents of the file should be '(.*?)'$/ do |contents| 13 | @uploader.read.chomp.should == contents 14 | end 15 | 16 | Given /^that the uploader reverses the filename$/ do 17 | @klass.class_eval do 18 | def filename 19 | super.reverse unless super.blank? 20 | end 21 | end 22 | end 23 | 24 | Given /^that the uploader has the filename overridden to '(.*?)'$/ do |filename| 25 | @klass.class_eval do 26 | define_method(:filename) do 27 | filename 28 | end 29 | end 30 | end 31 | 32 | Given /^that the uploader has the store_dir overridden to '(.*?)'$/ do |store_dir| 33 | @klass.class_eval do 34 | define_method(:store_dir) do 35 | file_path(store_dir) 36 | end 37 | end 38 | end 39 | 40 | Given /^that the version '(.*?)' has the store_dir overridden to '(.*?)'$/ do |version, store_dir| 41 | @klass.versions[version.to_sym][:uploader].class_eval do 42 | define_method(:store_dir) do 43 | file_path(store_dir) 44 | end 45 | end 46 | end 47 | 48 | Given /^that the uploader class has a version named '(.*?)'$/ do |name| 49 | @klass.version(name) 50 | end 51 | 52 | Given /^yo dawg, I put a version called '(.*?)' in your version called '(.*?)'$/ do |v2, v1| 53 | @klass.version(v1) do 54 | version(v2) 55 | end 56 | end 57 | 58 | Given /^the class has a method called 'reverse' that reverses the contents of a file$/ do 59 | @klass.class_eval do 60 | def reverse 61 | text = File.read(current_path) 62 | File.open(current_path, 'w') { |f| f.write(text.reverse) } 63 | end 64 | end 65 | end 66 | 67 | Given /^the class will process '([a-zA-Z0-9\_\?!]*)'$/ do |name| 68 | @klass.process name.to_sym 69 | end 70 | 71 | Then /^the uploader should have '(.*?)' as its current path$/ do |path| 72 | @uploader.current_path.should == file_path(path) 73 | end 74 | 75 | Then /^the uploader should have the url '(.*?)'$/ do |url| 76 | @uploader.url.should == url 77 | end 78 | 79 | Then /^the uploader's version '(.*?)' should have the url '(.*?)'$/ do |version, url| 80 | @uploader.versions[version.to_sym].url.should == url 81 | end 82 | 83 | Then /^the uploader's nested version '(.*?)' nested in '(.*?)' should have the url '(.*?)'$/ do |v2, v1, url| 84 | @uploader.versions[v1.to_sym].versions[v2.to_sym].url.should == url 85 | end 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | next if condition && !self.send(condition, new_file) 83 | self.send(method, *args) 84 | end 85 | end 86 | end 87 | 88 | end # Processing 89 | end # Uploader 90 | end # CarrierWave 91 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 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 61 | delete_cache_id if delete_cache_id_after_storage 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.join(cache_dir, @cache_id) 74 | FileUtils.rm_rf(path) if File.exists?(path) && File.directory?(path) 75 | end 76 | end 77 | 78 | ## 79 | # Retrieves the file from the storage. 80 | # 81 | # === Parameters 82 | # 83 | # [identifier (String)] uniquely identifies the file to retrieve 84 | # 85 | def retrieve_from_store!(identifier) 86 | with_callbacks(:retrieve_from_store, identifier) do 87 | @file = storage.retrieve!(identifier) 88 | end 89 | end 90 | 91 | private 92 | 93 | def full_filename(for_file) 94 | for_file 95 | end 96 | 97 | def storage 98 | @storage ||= self.class.storage.new(self) 99 | end 100 | 101 | end # Store 102 | end # Uploader 103 | end # CarrierWave 104 | -------------------------------------------------------------------------------- /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 :s3 55 | subclass = Class.new(@uploader_class) 56 | subclass.storage.should == CarrierWave::Storage::S3 57 | end 58 | 59 | it "should be changeable when inherited" do 60 | @uploader_class.storage :s3 61 | subclass = Class.new(@uploader_class) 62 | subclass.storage.should == CarrierWave::Storage::S3 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 | -------------------------------------------------------------------------------- /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 | # not sure why we need to do this 16 | require 'sqlite3/sqlite3_native' 17 | require 'sqlite3' 18 | 19 | require 'fog' 20 | require 'storage/fog_helper' 21 | 22 | unless ENV['REMOTE'] == 'true' 23 | Fog.mock! 24 | end 25 | 26 | require 'fog_credentials' # after Fog.mock! 27 | 28 | CARRIERWAVE_DIRECTORY = "carrierwave#{Time.now.to_i}" unless defined?(CARRIERWAVE_DIRECTORY) 29 | 30 | alias :running :lambda 31 | 32 | def file_path( *paths ) 33 | File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', *paths)) 34 | end 35 | 36 | def public_path( *paths ) 37 | File.expand_path(File.join(File.dirname(__FILE__), 'public', *paths)) 38 | end 39 | 40 | CarrierWave.root = public_path 41 | 42 | module CarrierWave 43 | module Test 44 | module MockStorage 45 | def mock_storage(kind) 46 | storage = mock("storage for #{kind} uploader") 47 | storage.stub!(:setup!) 48 | storage 49 | end 50 | end 51 | 52 | module MockFiles 53 | def stub_merb_tempfile(filename) 54 | raise "#{path} file does not exist" unless File.exist?(file_path(filename)) 55 | 56 | t = Tempfile.new(filename) 57 | FileUtils.copy_file(file_path(filename), t.path) 58 | 59 | return t 60 | end 61 | 62 | def stub_tempfile(filename, mime_type=nil, fake_name=nil) 63 | raise "#{path} file does not exist" unless File.exist?(file_path(filename)) 64 | 65 | t = Tempfile.new(filename) 66 | FileUtils.copy_file(file_path(filename), t.path) 67 | 68 | # This is stupid, but for some reason rspec won't play nice... 69 | eval <<-EOF 70 | def t.original_filename; '#{fake_name || filename}'; end 71 | def t.content_type; '#{mime_type}'; end 72 | def t.local_path; path; end 73 | EOF 74 | 75 | return t 76 | end 77 | 78 | def stub_stringio(filename, mime_type=nil, fake_name=nil) 79 | if filename 80 | t = StringIO.new( IO.read( file_path( filename ) ) ) 81 | else 82 | t = StringIO.new 83 | end 84 | t.stub!(:local_path).and_return("") 85 | t.stub!(:original_filename).and_return(filename || fake_name) 86 | t.stub!(:content_type).and_return(mime_type) 87 | return t 88 | end 89 | 90 | def stub_file(filename, mime_type=nil, fake_name=nil) 91 | f = File.open(file_path(filename)) 92 | return f 93 | end 94 | end 95 | 96 | module I18nHelpers 97 | def change_locale_and_store_translations(locale, translations, &block) 98 | current_locale = I18n.locale 99 | begin 100 | I18n.backend.store_translations locale, translations 101 | I18n.locale = locale 102 | yield 103 | ensure 104 | I18n.reload! 105 | I18n.locale = current_locale 106 | end 107 | end 108 | end 109 | end 110 | end 111 | 112 | RSpec.configure do |config| 113 | config.include CarrierWave::Test::Matchers 114 | config.include CarrierWave::Test::MockFiles 115 | config.include CarrierWave::Test::MockStorage 116 | config.include CarrierWave::Test::I18nHelpers 117 | end 118 | -------------------------------------------------------------------------------- /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/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/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 | context "with 'enable_processing' set to false" do 69 | it "should not do any processing" do 70 | @uploader_class.enable_processing = false 71 | @uploader_class.process :sepiatone, :desaturate, :invert 72 | @uploader.should_not_receive(:sepiatone) 73 | @uploader.should_not_receive(:desaturate) 74 | @uploader.should_not_receive(:invert) 75 | @uploader.process! 76 | end 77 | end 78 | end 79 | 80 | describe '#cache!' do 81 | before do 82 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 83 | end 84 | 85 | it "should trigger a process!" do 86 | @uploader.should_receive(:process!) 87 | @uploader.cache!(File.open(file_path('test.jpg'))) 88 | end 89 | end 90 | 91 | describe '#recreate_versions!' do 92 | before do 93 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 94 | end 95 | 96 | it "should trigger a process!" do 97 | @uploader.store!(File.open(file_path('test.jpg'))) 98 | @uploader.should_receive(:process!) 99 | @uploader.recreate_versions! 100 | end 101 | end 102 | 103 | end 104 | -------------------------------------------------------------------------------- /spec/storage/cloudfiles_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | if ENV['REMOTE'] == 'true' 6 | require 'cloudfiles' 7 | require 'net/http' 8 | 9 | class CloudfilesSpecUploader < CarrierWave::Uploader::Base 10 | storage :cloud_files 11 | end 12 | 13 | describe CarrierWave::Storage::CloudFiles do 14 | before do 15 | @credentials = FOG_CREDENTIALS.select {|c| c[:provider] == 'Rackspace'}.first 16 | @container_name = "#{CARRIERWAVE_DIRECTORY}cloudfiles" 17 | @connection = CloudFiles::Connection.new(@credentials[:rackspace_username], @credentials[:rackspace_api_key]) 18 | @connection.create_container(@container_name) unless @connection.container_exists?(@container_name) 19 | @container = @connection.container(@container_name) 20 | @container.make_public 21 | 22 | CarrierWave.configure do |config| 23 | config.reset_config 24 | config.cloud_files_username = @credentials[:rackspace_username] 25 | config.cloud_files_api_key = @credentials[:rackspace_api_key] 26 | config.cloud_files_container = @container_name 27 | end 28 | 29 | @uploader = CloudfilesSpecUploader.new 30 | @storage = CarrierWave::Storage::CloudFiles.new(@uploader) 31 | @file = stub_tempfile('test.jpg', 'application/xml') 32 | end 33 | 34 | describe '#store!' do 35 | before do 36 | @uploader.stub!(:store_path).and_return('uploads/bar.txt') 37 | @cloud_file = @storage.store!(@file) 38 | end 39 | 40 | it "should upload the file to Cloud Files" do 41 | @container.object('uploads/bar.txt').data.should == 'this is stuff' 42 | end 43 | 44 | it "should have a path" do 45 | @cloud_file.path.should == 'uploads/bar.txt' 46 | end 47 | 48 | it "should have an Rackspace URL" do 49 | # Don't check if its ".cdn." or ".cdn2." because they change these URLs 50 | @cloud_file.url.should =~ %r!http://(.*?).rackcdn.com/uploads/bar.txt! 51 | end 52 | 53 | it "should store the content type on Cloud Files" do 54 | # Recent addition of the charset to the response 55 | @cloud_file.content_type.should == 'application/xml; charset=UTF-8' 56 | end 57 | 58 | it "should be deletable" do 59 | @cloud_file.delete 60 | @container.object_exists?('uploads/bar.txt').should be_false 61 | end 62 | 63 | end 64 | 65 | describe '#retrieve!' do 66 | before do 67 | @container.create_object('uploads/bar.txt').write("A test, 1234") 68 | @uploader.stub!(:store_path).with('bar.txt').and_return('uploads/bar.txt') 69 | 70 | @cloud_file = @storage.retrieve!('bar.txt') 71 | end 72 | 73 | it "should retrieve the file contents from Cloud Files" do 74 | @cloud_file.read.chomp.should == "A test, 1234" 75 | end 76 | 77 | it "should have a path" do 78 | @cloud_file.path.should == 'uploads/bar.txt' 79 | end 80 | 81 | it "should have an Rackspace URL" do 82 | # Don't check if its ".cdn." or ".cdn2." because they change these URLs 83 | @cloud_file.url.should =~ %r!http://(.*?).rackcdn.com/uploads/bar.txt! 84 | end 85 | 86 | it "should allow for configured CDN urls" do 87 | @uploader.stub!(:cloud_files_cdn_host).and_return("cdn.com") 88 | @cloud_file.url.should == 'http://cdn.com/uploads/bar.txt' 89 | end 90 | 91 | it "should be deletable" do 92 | @cloud_file.delete 93 | @container.object_exists?('uploads/bar.txt').should be_false 94 | end 95 | end 96 | 97 | describe 'finished' do 98 | it "should delete the container when finished" do 99 | @connection.delete_container(@container_name) 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /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 | 7 | begin 8 | require 'active_support/core_ext/class/inheritable_attributes' 9 | rescue LoadError 10 | end 11 | 12 | require 'active_support/concern' 13 | require 'active_support/memoizable' 14 | 15 | module CarrierWave 16 | 17 | class << self 18 | attr_accessor :root 19 | 20 | def configure(&block) 21 | CarrierWave::Uploader::Base.configure(&block) 22 | end 23 | 24 | def clean_cached_files! 25 | CarrierWave::Uploader::Base.clean_cached_files! 26 | end 27 | end 28 | 29 | class UploadError < StandardError; end 30 | class IntegrityError < UploadError; end 31 | class InvalidParameter < UploadError; end 32 | class ProcessingError < UploadError; end 33 | class DownloadError < UploadError; end 34 | 35 | autoload :SanitizedFile, 'carrierwave/sanitized_file' 36 | autoload :Mount, 'carrierwave/mount' 37 | autoload :RMagick, 'carrierwave/processing/rmagick' 38 | autoload :ImageScience, 'carrierwave/processing/image_science' 39 | autoload :MiniMagick, 'carrierwave/processing/mini_magick' 40 | autoload :MimeTypes, 'carrierwave/processing/mime_types' 41 | autoload :VERSION, 'carrierwave/version' 42 | 43 | module Storage 44 | autoload :Abstract, 'carrierwave/storage/abstract' 45 | autoload :File, 'carrierwave/storage/file' 46 | autoload :Fog, 'carrierwave/storage/fog' 47 | autoload :S3, 'carrierwave/storage/s3' 48 | autoload :GridFS, 'carrierwave/storage/grid_fs' 49 | autoload :RightS3, 'carrierwave/storage/right_s3' 50 | autoload :CloudFiles, 'carrierwave/storage/cloud_files' 51 | end 52 | 53 | module Uploader 54 | autoload :Base, 'carrierwave/uploader' 55 | autoload :Cache, 'carrierwave/uploader/cache' 56 | autoload :Store, 'carrierwave/uploader/store' 57 | autoload :Download, 'carrierwave/uploader/download' 58 | autoload :Callbacks, 'carrierwave/uploader/callbacks' 59 | autoload :Processing, 'carrierwave/uploader/processing' 60 | autoload :Versions, 'carrierwave/uploader/versions' 61 | autoload :Remove, 'carrierwave/uploader/remove' 62 | autoload :ExtensionWhitelist, 'carrierwave/uploader/extension_whitelist' 63 | autoload :DefaultUrl, 'carrierwave/uploader/default_url' 64 | autoload :Proxy, 'carrierwave/uploader/proxy' 65 | autoload :Url, 'carrierwave/uploader/url' 66 | autoload :Mountable, 'carrierwave/uploader/mountable' 67 | autoload :Configuration, 'carrierwave/uploader/configuration' 68 | end 69 | 70 | module Compatibility 71 | autoload :Paperclip, 'carrierwave/compatibility/paperclip' 72 | end 73 | 74 | module Test 75 | autoload :Matchers, 'carrierwave/test/matchers' 76 | end 77 | 78 | end 79 | 80 | if defined?(Merb) 81 | 82 | CarrierWave.root = Merb.dir_for(:public) 83 | Merb::BootLoader.before_app_loads do 84 | # Setup path for uploaders and load all of them before classes are loaded 85 | Merb.push_path(:uploaders, Merb.root / 'app' / 'uploaders', '*.rb') 86 | Dir.glob(File.join(Merb.load_paths[:uploaders])).each {|f| require f } 87 | end 88 | 89 | elsif defined?(Rails) 90 | 91 | module CarrierWave 92 | class Railtie < Rails::Railtie 93 | initializer "carrierwave.setup_paths" do 94 | CarrierWave.root = Rails.root.join(Rails.public_path).to_s 95 | end 96 | 97 | initializer "carrierwave.active_record" do 98 | ActiveSupport.on_load :active_record do 99 | require 'carrierwave/orm/activerecord' 100 | end 101 | end 102 | end 103 | end 104 | 105 | elsif defined?(Sinatra) 106 | 107 | CarrierWave.root = Sinatra::Application.public 108 | 109 | end 110 | 111 | require 'carrierwave/orm/datamapper' if defined?(DataMapper) 112 | require 'carrierwave/orm/sequel' if defined?(Sequel) 113 | require 'carrierwave/orm/mongoid' if defined?(Mongoid) 114 | -------------------------------------------------------------------------------- /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 | @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 '#url' do 18 | before do 19 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 20 | end 21 | 22 | it "should default to nil" do 23 | @uploader.url.should be_nil 24 | end 25 | 26 | it "should raise ArgumentError when version doesn't exist" do 27 | lambda { @uploader.url(:thumb) }.should raise_error(ArgumentError) 28 | end 29 | 30 | it "should not raise ArgumentError when versions version exists" do 31 | @uploader_class.version(:thumb) 32 | lambda { @uploader.url(:thumb) }.should_not raise_error(ArgumentError) 33 | end 34 | 35 | it "should get the directory relative to public, prepending a slash" do 36 | @uploader.cache!(File.open(file_path('test.jpg'))) 37 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 38 | end 39 | 40 | it "should get the directory relative to public for a specific version" do 41 | @uploader_class.version(:thumb) 42 | @uploader.cache!(File.open(file_path('test.jpg'))) 43 | @uploader.url(:thumb).should == '/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg' 44 | end 45 | 46 | it "should get the directory relative to public for a nested version" do 47 | @uploader_class.version(:thumb) do 48 | version(:mini) 49 | end 50 | @uploader.cache!(File.open(file_path('test.jpg'))) 51 | @uploader.url(:thumb, :mini).should == '/uploads/tmp/20071201-1234-345-2255/thumb_mini_test.jpg' 52 | end 53 | 54 | it "should return file#url if available" do 55 | @uploader.cache!(File.open(file_path('test.jpg'))) 56 | @uploader.file.stub!(:url).and_return('http://www.example.com/someurl.jpg') 57 | @uploader.url.should == 'http://www.example.com/someurl.jpg' 58 | end 59 | 60 | it "should get the directory relative to public, if file#url is blank" do 61 | @uploader.cache!(File.open(file_path('test.jpg'))) 62 | @uploader.file.stub!(:url).and_return('') 63 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 64 | end 65 | end 66 | 67 | describe '#to_json' do 68 | before do 69 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 70 | end 71 | 72 | it "should return a hash with a blank URL" do 73 | JSON.parse(@uploader.to_json)['url'].should be_nil 74 | end 75 | 76 | it "should return a hash including a cached URL" do 77 | @uploader.cache!(File.open(file_path('test.jpg'))) 78 | JSON.parse(@uploader.to_json)['url'].should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 79 | end 80 | 81 | it "should return a hash including a cached URL of a version" do 82 | @uploader_class.version :thumb 83 | @uploader.cache!(File.open(file_path('test.jpg'))) 84 | JSON.parse(@uploader.to_json)['thumb']['url'].should == '/uploads/tmp/20071201-1234-345-2255/thumb_test.jpg' 85 | end 86 | end 87 | 88 | describe '#to_s' do 89 | before do 90 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 91 | end 92 | 93 | it "should default to nil" do 94 | @uploader.to_s.should be_nil 95 | end 96 | 97 | it "should get the directory relative to public, prepending a slash" do 98 | @uploader.cache!(File.open(file_path('test.jpg'))) 99 | @uploader.to_s.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 100 | end 101 | 102 | it "should return file#url if available" do 103 | @uploader.cache!(File.open(file_path('test.jpg'))) 104 | @uploader.file.stub!(:url).and_return('http://www.example.com/someurl.jpg') 105 | @uploader.to_s.should == 'http://www.example.com/someurl.jpg' 106 | end 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/grid_fs.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'mongo' 3 | 4 | module CarrierWave 5 | module Storage 6 | 7 | ## 8 | # The GridFS store uses MongoDB's GridStore file storage system to store files 9 | # 10 | # There are two ways of configuring the GridFS connection. Either you create a 11 | # connection or you reuse an existing connection. 12 | # 13 | # Creating a connection looks something like this: 14 | # 15 | # CarrierWave.configure do |config| 16 | # config.storage = :grid_fs 17 | # config.grid_fs_host = "your-host.com" 18 | # config.grid_fs_port = "27017" 19 | # config.grid_fs_database = "your_dbs_app_name" 20 | # config.grid_fs_username = "user" 21 | # config.grid_fs_password = "verysecret" 22 | # config.grid_fs_access_url = "/images" 23 | # end 24 | # 25 | # In the above example your documents url will look like: 26 | # 27 | # http://your-app.com/images/:document-identifier-here 28 | # 29 | # When you already have a Mongo connection object (for example through Mongoid) 30 | # you can also reuse this connection: 31 | # 32 | # CarrierWave.configure do |config| 33 | # config.storage = :grid_fs 34 | # config.grid_fs_connection = Mongoid.database 35 | # config.grid_fs_access_url = "/images" 36 | # end 37 | # 38 | class GridFS < Abstract 39 | 40 | class File 41 | 42 | def initialize(uploader, path) 43 | @path = path 44 | @uploader = uploader 45 | end 46 | 47 | def path 48 | @path 49 | end 50 | 51 | def url 52 | unless @uploader.grid_fs_access_url 53 | nil 54 | else 55 | [@uploader.grid_fs_access_url, @path].join("/") 56 | end 57 | end 58 | 59 | def read 60 | grid.open(@path, 'r').data 61 | end 62 | 63 | def write(file) 64 | grid.open(@uploader.store_path, 'w', :content_type => file.content_type) do |f| 65 | f.write(file.read) 66 | end 67 | end 68 | 69 | def delete 70 | grid.delete(@path) 71 | end 72 | 73 | def content_type 74 | grid.open(@path, 'r').content_type 75 | end 76 | 77 | def file_length 78 | grid.open(@path, 'r').file_length 79 | end 80 | 81 | protected 82 | 83 | def database 84 | @connection ||= @uploader.grid_fs_connection || begin 85 | host = @uploader.grid_fs_host 86 | port = @uploader.grid_fs_port 87 | database = @uploader.grid_fs_database 88 | username = @uploader.grid_fs_username 89 | password = @uploader.grid_fs_password 90 | db = Mongo::Connection.new(host, port).db(database) 91 | db.authenticate(username, password) if username && password 92 | db 93 | end 94 | end 95 | 96 | def grid 97 | @grid ||= Mongo::GridFileSystem.new(database) 98 | end 99 | 100 | end 101 | 102 | ## 103 | # Store the file in MongoDB's GridFS GridStore 104 | # 105 | # === Parameters 106 | # 107 | # [file (CarrierWave::SanitizedFile)] the file to store 108 | # 109 | # === Returns 110 | # 111 | # [CarrierWave::SanitizedFile] a sanitized file 112 | # 113 | def store!(file) 114 | stored = CarrierWave::Storage::GridFS::File.new(uploader, uploader.store_path) 115 | stored.write(file) 116 | stored 117 | end 118 | 119 | ## 120 | # Retrieve the file from MongoDB's GridFS GridStore 121 | # 122 | # === Parameters 123 | # 124 | # [identifier (String)] the filename of the file 125 | # 126 | # === Returns 127 | # 128 | # [CarrierWave::Storage::GridFS::File] a sanitized file 129 | # 130 | def retrieve!(identifier) 131 | CarrierWave::Storage::GridFS::File.new(uploader, uploader.store_path(identifier)) 132 | end 133 | 134 | end # File 135 | end # Storage 136 | end # CarrierWave 137 | -------------------------------------------------------------------------------- /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 raise an error when trying to download a local file" do 77 | running { 78 | @uploader.download!('/etc/passwd') 79 | }.should raise_error(CarrierWave::DownloadError) 80 | end 81 | 82 | it "should accept spaces in the url" do 83 | @uploader.download!('http://www.example.com/test with spaces/test.jpg') 84 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 85 | end 86 | 87 | it "should follow redirects" do 88 | @uploader.download!('http://www.redirect.com/') 89 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 90 | end 91 | 92 | describe '#download! with an extension_white_list' do 93 | before do 94 | @uploader_class.class_eval do 95 | def extension_white_list 96 | %w(txt) 97 | end 98 | end 99 | end 100 | 101 | it "should follow redirects but still respect the extension_white_list" do 102 | running { 103 | @uploader.download!('http://www.redirect.com/') 104 | }.should raise_error(CarrierWave::IntegrityError) 105 | end 106 | end 107 | end 108 | 109 | describe '#download! with an overridden process_uri method' do 110 | before do 111 | @uploader_class.class_eval do 112 | def process_uri(uri) 113 | raise CarrierWave::DownloadError 114 | end 115 | end 116 | end 117 | 118 | it "should allow overriding the process_uri method" do 119 | running { 120 | @uploader.download!('http://www.example.com/test.jpg') 121 | }.should raise_error(CarrierWave::DownloadError) 122 | end 123 | end 124 | 125 | describe '#process_uri' do 126 | let(:uri) { "http://www.example.com/test%20image.jpg" } 127 | 128 | it 'should unescape and then escape the given uri' do 129 | unescaped_uri = URI.unescape(uri) 130 | @uploader.process_uri(unescaped_uri).should == @uploader.process_uri(uri) 131 | end 132 | 133 | it 'should parse the given uri' do 134 | @uploader.process_uri(uri).should == URI.parse(uri) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/carrierwave/processing/image_science.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'image_science' 4 | 5 | module CarrierWave 6 | module ImageScience 7 | extend ActiveSupport::Concern 8 | 9 | module ClassMethods 10 | def resize_to_limit(width, height) 11 | process :resize_to_limit => [width, height] 12 | end 13 | 14 | def resize_to_fit(width, height) 15 | process :resize_to_fit => [width, height] 16 | end 17 | 18 | def resize_to_fill(width, height) 19 | process :resize_to_fill => [width, height] 20 | end 21 | end 22 | 23 | ## 24 | # Resize the image to fit within the specified dimensions while retaining 25 | # the original aspect ratio. The image may be shorter or narrower than 26 | # specified in the smaller dimension but will not be larger than the 27 | # specified values. 28 | # 29 | # === Parameters 30 | # 31 | # [width (Integer)] the width to scale the image to 32 | # [height (Integer)] the height to scale the image to 33 | # 34 | def resize_to_fit(new_width, new_height) 35 | cache_stored_file! if !cached? 36 | 37 | ::ImageScience.with_image(self.current_path) do |img| 38 | width, height = extract_dimensions(img.width, img.height, new_width, new_height) 39 | img.resize( width, height ) do |file| 40 | file.save( self.current_path ) 41 | end 42 | end 43 | end 44 | 45 | ## 46 | # Resize the image to fit within the specified dimensions while retaining 47 | # the aspect ratio of the original image. If necessary, crop the image in 48 | # the larger dimension. 49 | # 50 | # === Parameters 51 | # 52 | # [width (Integer)] the width to scale the image to 53 | # [height (Integer)] the height to scale the image to 54 | # 55 | def resize_to_fill(new_width, new_height) 56 | cache_stored_file! if !cached? 57 | 58 | ::ImageScience.with_image(self.current_path) do |img| 59 | width, height = extract_dimensions_for_crop(img.width, img.height, new_width, new_height) 60 | x_offset, y_offset = extract_placement_for_crop(width, height, new_width, new_height) 61 | 62 | # check if if new dimensions are too small for the new image 63 | if width < new_width 64 | width = new_width 65 | height = (new_width.to_f*(img.height.to_f/img.width.to_f)).round 66 | elsif height < new_height 67 | height = new_height 68 | width = (new_height.to_f*(img.width.to_f/img.height.to_f)).round 69 | end 70 | 71 | img.resize( width, height ) do |i2| 72 | 73 | # check to make sure offset is not negative 74 | if x_offset < 0 75 | x_offset = 0 76 | end 77 | if y_offset < 0 78 | y_offset = 0 79 | end 80 | 81 | i2.with_crop( x_offset, y_offset, new_width + x_offset, new_height + y_offset) do |file| 82 | file.save( self.current_path ) 83 | end 84 | end 85 | end 86 | end 87 | 88 | ## 89 | # Resize the image to fit within the specified dimensions while retaining 90 | # the original aspect ratio. Will only resize the image if it is larger than the 91 | # specified dimensions. The resulting image may be shorter or narrower than specified 92 | # in the smaller dimension but will not be larger than the specified values. 93 | # 94 | # === Parameters 95 | # 96 | # [width (Integer)] the width to scale the image to 97 | # [height (Integer)] the height to scale the image to 98 | # 99 | def resize_to_limit(new_width, new_height) 100 | cache_stored_file! if !cached? 101 | 102 | ::ImageScience.with_image(self.current_path) do |img| 103 | if img.width > new_width or img.height > new_height 104 | resize_to_fit(new_width, new_height) 105 | end 106 | end 107 | end 108 | 109 | private 110 | 111 | def extract_dimensions(width, height, new_width, new_height, type = :resize) 112 | aspect_ratio = width.to_f / height.to_f 113 | new_aspect_ratio = new_width / new_height 114 | 115 | if (new_aspect_ratio > aspect_ratio) ^ ( type == :crop ) # Image is too wide, the caret is the XOR operator 116 | new_width, new_height = [ (new_height * aspect_ratio), new_height] 117 | else #Image is too narrow 118 | new_width, new_height = [ new_width, (new_width / aspect_ratio)] 119 | end 120 | 121 | [new_width, new_height].collect! { |v| v.round } 122 | end 123 | 124 | def extract_dimensions_for_crop(width, height, new_width, new_height) 125 | extract_dimensions(width, height, new_width, new_height, :crop) 126 | end 127 | 128 | def extract_placement_for_crop(width, height, new_width, new_height) 129 | x_offset = (width / 2.0) - (new_width / 2.0) 130 | y_offset = (height / 2.0) - (new_height / 2.0) 131 | [x_offset, y_offset].collect! { |v| v.round } 132 | end 133 | 134 | end # ImageScience 135 | end # CarrierWave 136 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | def matches?(actual) 17 | @actual = actual 18 | FileUtils.identical?(@actual, @expected) 19 | end 20 | def failure_message 21 | "expected #{@actual.inspect} to be identical to #{@expected.inspect}" 22 | end 23 | def negative_failure_message 24 | "expected #{@actual.inspect} to not be identical to #{@expected.inspect}" 25 | end 26 | end 27 | 28 | def be_identical_to(expected) 29 | BeIdenticalTo.new(expected) 30 | end 31 | 32 | class HavePermissions # :nodoc: 33 | def initialize(expected) 34 | @expected = expected 35 | end 36 | 37 | def matches?(actual) 38 | @actual = actual 39 | # Satisfy expectation here. Return false or raise an error if it's not met. 40 | (File.stat(@actual.path).mode & 0777) == @expected 41 | end 42 | 43 | def failure_message 44 | "expected #{@actual.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0777).to_s(8)}" 45 | end 46 | 47 | def negative_failure_message 48 | "expected #{@actual.inspect} not to have permissions #{@expected.to_s(8)}, but it did" 49 | end 50 | end 51 | 52 | def have_permissions(expected) 53 | HavePermissions.new(expected) 54 | end 55 | 56 | class BeNoLargerThan # :nodoc: 57 | def initialize(width, height) 58 | @width, @height = width, height 59 | end 60 | 61 | def matches?(actual) 62 | @actual = actual 63 | # Satisfy expectation here. Return false or raise an error if it's not met. 64 | image = ImageLoader.load_image(@actual.current_path) 65 | @actual_width = image.width 66 | @actual_height = image.height 67 | @actual_width <= @width && @actual_height <= @height 68 | end 69 | 70 | def failure_message 71 | "expected #{@actual.current_path.inspect} to be no larger than #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}." 72 | end 73 | 74 | def negative_failure_message 75 | "expected #{@actual.current_path.inspect} to be larger than #{@width} by #{@height}, but it wasn't." 76 | end 77 | 78 | end 79 | 80 | def be_no_larger_than(width, height) 81 | BeNoLargerThan.new(width, height) 82 | end 83 | 84 | class HaveDimensions # :nodoc: 85 | def initialize(width, height) 86 | @width, @height = width, height 87 | end 88 | 89 | def matches?(actual) 90 | @actual = actual 91 | # Satisfy expectation here. Return false or raise an error if it's not met. 92 | image = ImageLoader.load_image(@actual.current_path) 93 | @actual_width = image.width 94 | @actual_height = image.height 95 | @actual_width == @width && @actual_height == @height 96 | end 97 | 98 | def failure_message 99 | "expected #{@actual.current_path.inspect} to have an exact size of #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}." 100 | end 101 | 102 | def negative_failure_message 103 | "expected #{@actual.current_path.inspect} not to have an exact size of #{@width} by #{@height}, but it did." 104 | end 105 | 106 | end 107 | 108 | def have_dimensions(width, height) 109 | HaveDimensions.new(width, height) 110 | end 111 | 112 | class ImageLoader # :nodoc: 113 | def self.load_image(filename) 114 | if defined? ::MiniMagick 115 | MiniMagickWrapper.new(filename) 116 | else 117 | unless defined? ::Magick 118 | begin 119 | require 'rmagick' 120 | rescue LoadError 121 | require 'RMagick' 122 | rescue LoadError 123 | puts "WARNING: Failed to require rmagick, image processing may fail!" 124 | end 125 | end 126 | MagickWrapper.new(filename) 127 | end 128 | end 129 | end 130 | 131 | class MagickWrapper # :nodoc: 132 | attr_reader :image 133 | def width 134 | image.columns 135 | end 136 | 137 | def height 138 | image.rows 139 | end 140 | 141 | def initialize(filename) 142 | @image = ::Magick::Image.read(filename).first 143 | end 144 | end 145 | 146 | class MiniMagickWrapper # :nodoc: 147 | attr_reader :image 148 | def width 149 | image[:width] 150 | end 151 | 152 | def height 153 | image[:height] 154 | end 155 | 156 | def initialize(filename) 157 | @image = ::MiniMagick::Image.open(filename) 158 | end 159 | end 160 | 161 | end # Matchers 162 | end # Test 163 | end # CarrierWave 164 | 165 | -------------------------------------------------------------------------------- /spec/storage/grid_fs_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'mongo' 5 | 6 | shared_examples_for "a GridFS connection" do 7 | describe '#store!' do 8 | before do 9 | @uploader.stub!(:store_path).and_return('uploads/bar.txt') 10 | @grid_fs_file = @storage.store!(@file) 11 | end 12 | 13 | it "should upload the file to gridfs" do 14 | @grid.open('uploads/bar.txt', 'r').data.should == 'this is stuff' 15 | end 16 | 17 | it "should have the same path that it was stored as" do 18 | @grid_fs_file.path.should == 'uploads/bar.txt' 19 | end 20 | 21 | it "should read the contents of the file" do 22 | @grid_fs_file.read.should == "this is stuff" 23 | end 24 | 25 | it "should not have a URL" do 26 | @grid_fs_file.url.should be_nil 27 | end 28 | 29 | it "should be deletable" do 30 | @grid_fs_file.delete 31 | lambda {@grid.open('uploads/bar.txt', 'r')}.should raise_error(Mongo::GridFileNotFound) 32 | end 33 | 34 | it "should store the content type on GridFS" do 35 | @grid_fs_file.content_type.should == 'application/xml' 36 | end 37 | 38 | it "should have a file length" do 39 | @grid_fs_file.file_length.should == 13 40 | end 41 | 42 | end 43 | 44 | describe '#retrieve!' do 45 | before do 46 | @grid.open('uploads/bar.txt', 'w') { |f| f.write "A test, 1234" } 47 | @uploader.stub!(:store_path).with('bar.txt').and_return('uploads/bar.txt') 48 | @grid_fs_file = @storage.retrieve!('bar.txt') 49 | end 50 | 51 | it "should retrieve the file contents from gridfs" do 52 | @grid_fs_file.read.chomp.should == "A test, 1234" 53 | end 54 | 55 | it "should have the same path that it was stored as" do 56 | @grid_fs_file.path.should == 'uploads/bar.txt' 57 | end 58 | 59 | it "should not have a URL unless set" do 60 | @grid_fs_file.url.should be_nil 61 | end 62 | 63 | it "should return a URL if configured" do 64 | @uploader.stub!(:grid_fs_access_url).and_return("/image/show") 65 | @grid_fs_file.url.should == "/image/show/uploads/bar.txt" 66 | end 67 | 68 | it "should be deletable" do 69 | @grid_fs_file.delete 70 | lambda {@grid.open('uploads/bar.txt', 'r')}.should raise_error(Mongo::GridFileNotFound) 71 | end 72 | end 73 | 74 | end 75 | 76 | describe CarrierWave::Storage::GridFS do 77 | 78 | before do 79 | @database = Mongo::Connection.new('localhost', 27017).db('carrierwave_test') 80 | 81 | @uploader = mock('an uploader') 82 | @uploader.stub!(:grid_fs_access_url).and_return(nil) 83 | end 84 | 85 | context "when reusing an existing connection manually" do 86 | before do 87 | @uploader.stub!(:grid_fs_connection).and_return(@database) 88 | 89 | @grid = Mongo::GridFileSystem.new(@database) 90 | 91 | @storage = CarrierWave::Storage::GridFS.new(@uploader) 92 | @file = stub_tempfile('test.jpg', 'application/xml') 93 | end 94 | 95 | it_should_behave_like "a GridFS connection" 96 | 97 | # Calling #recreate_versions! on uploaders has been known to fail on 98 | # remotely hosted files. This is due to a variety of issues, but this test 99 | # makes sure that there's no unnecessary errors during the process 100 | describe "#recreate_versions!" do 101 | before do 102 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 103 | @uploader_class.class_eval{ 104 | include CarrierWave::MiniMagick 105 | storage :grid_fs 106 | 107 | process :resize_to_fit => [10, 10] 108 | } 109 | 110 | @versioned = @uploader_class.new 111 | @versioned.stub!(:grid_fs_connection).and_return(@database) 112 | 113 | @versioned.store! File.open(file_path('portrait.jpg')) 114 | end 115 | 116 | after do 117 | FileUtils.rm_rf(public_path) 118 | end 119 | 120 | it "recreates versions stored remotely without error" do 121 | lambda { 122 | @versioned.recreate_versions! 123 | }.should_not raise_error 124 | 125 | @versioned.should be_present 126 | end 127 | end 128 | 129 | 130 | describe "resize_to_fill" do 131 | before do 132 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 133 | @uploader_class.class_eval{ 134 | include CarrierWave::MiniMagick 135 | storage :grid_fs 136 | } 137 | 138 | @versioned = @uploader_class.new 139 | @versioned.stub!(:grid_fs_connection).and_return(@database) 140 | 141 | @versioned.store! File.open(file_path('portrait.jpg')) 142 | end 143 | 144 | after do 145 | FileUtils.rm_rf(public_path) 146 | end 147 | 148 | it "resizes the file with out error" do 149 | lambda { 150 | @versioned.resize_to_fill(200, 200) 151 | }.should_not raise_error 152 | 153 | end 154 | end 155 | end 156 | 157 | context "when setting a connection manually" do 158 | before do 159 | @uploader.stub!(:grid_fs_database).and_return("carrierwave_test") 160 | @uploader.stub!(:grid_fs_host).and_return("localhost") 161 | @uploader.stub!(:grid_fs_port).and_return(27017) 162 | @uploader.stub!(:grid_fs_username).and_return(nil) 163 | @uploader.stub!(:grid_fs_password).and_return(nil) 164 | @uploader.stub!(:grid_fs_connection).and_return(nil) 165 | 166 | @grid = Mongo::GridFileSystem.new(@database) 167 | 168 | @storage = CarrierWave::Storage::GridFS.new(@uploader) 169 | @file = stub_tempfile('test.jpg', 'application/xml') 170 | end 171 | 172 | it_should_behave_like "a GridFS connection" 173 | end 174 | 175 | after do 176 | @grid.delete('uploads/bar.txt') 177 | end 178 | 179 | end 180 | -------------------------------------------------------------------------------- /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 | sanitized = SanitizedFile.new :tempfile => StringIO.new(file.read), 74 | :filename => File.basename(path), :content_type => file.content_type 75 | 76 | cache! sanitized 77 | end 78 | 79 | ## 80 | # Returns a String which uniquely identifies the currently cached file for later retrieval 81 | # 82 | # === Returns 83 | # 84 | # [String] a cache name, in the format YYYYMMDD-HHMM-PID-RND/filename.txt 85 | # 86 | def cache_name 87 | File.join(cache_id, full_original_filename) if cache_id and original_filename 88 | end 89 | 90 | ## 91 | # Caches the given file. Calls process! to trigger any process callbacks. 92 | # 93 | # === Parameters 94 | # 95 | # [new_file (File, IOString, Tempfile)] any kind of file object 96 | # 97 | # === Raises 98 | # 99 | # [CarrierWave::FormNotMultipart] if the assigned parameter is a string 100 | # 101 | def cache!(new_file) 102 | new_file = CarrierWave::SanitizedFile.new(new_file) 103 | 104 | unless new_file.empty? 105 | raise CarrierWave::FormNotMultipart if new_file.is_path? && ensure_multipart_form 106 | 107 | with_callbacks(:cache, new_file) do 108 | self.cache_id = CarrierWave.generate_cache_id unless cache_id 109 | 110 | @filename = new_file.filename 111 | self.original_filename = new_file.filename 112 | 113 | @file = new_file.copy_to(cache_path, permissions) 114 | end 115 | end 116 | end 117 | 118 | ## 119 | # Retrieves the file with the given cache_name from the cache. 120 | # 121 | # === Parameters 122 | # 123 | # [cache_name (String)] uniquely identifies a cache file 124 | # 125 | # === Raises 126 | # 127 | # [CarrierWave::InvalidParameter] if the cache_name is incorrectly formatted. 128 | # 129 | def retrieve_from_cache!(cache_name) 130 | with_callbacks(:retrieve_from_cache, cache_name) do 131 | self.cache_id, self.original_filename = cache_name.to_s.split('/', 2) 132 | @filename = original_filename 133 | 134 | if File.exist?(cache_path) && defined?(MIME::Types) 135 | @file = SanitizedFile.new( 136 | :tempfile => cache_path, 137 | :filename => @filename, 138 | :content_type => MIME::Types.of(File.basename(cache_path))[0].content_type 139 | ) 140 | else 141 | @file = CarrierWave::SanitizedFile.new(cache_path) 142 | end 143 | end 144 | end 145 | 146 | private 147 | 148 | def cache_path 149 | File.expand_path(File.join(cache_dir, cache_name), root) 150 | end 151 | 152 | attr_reader :cache_id, :original_filename 153 | 154 | # We can override the full_original_filename method in other modules 155 | alias_method :full_original_filename, :original_filename 156 | 157 | def cache_id=(cache_id) 158 | raise CarrierWave::InvalidParameter, "invalid cache id" unless cache_id =~ /\A[\d]{8}\-[\d]{4}\-[\d]+\-[\d]{4}\z/ 159 | @cache_id = cache_id 160 | end 161 | 162 | def original_filename=(filename) 163 | raise CarrierWave::InvalidParameter, "invalid filename" if filename =~ CarrierWave::SanitizedFile.sanitize_regexp 164 | @original_filename = filename 165 | end 166 | 167 | end # Cache 168 | end # Uploader 169 | end # CarrierWave 170 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/cloud_files.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'cloudfiles' 3 | 4 | module CarrierWave 5 | module Storage 6 | 7 | ## 8 | # Uploads things to Rackspace Cloud Files webservices using the Rackspace libraries (cloudfiles gem). 9 | # In order for CarrierWave to connect to Cloud Files, you'll need to specify an username, api key 10 | # and container. Optional arguments are config.cloud_files_snet (using the private internal 11 | # Rackspace network for communication) and config.cloud_files_auth_url (for connecting to Rackspace's 12 | # UK infrastructure or an OpenStack Swift installation) 13 | # 14 | # CarrierWave.configure do |config| 15 | # config.cloud_files_username = "xxxxxx" 16 | # config.cloud_files_api_key = "xxxxxx" 17 | # config.cloud_files_container = "my_container" 18 | # config.cloud_files_auth_url = "https://lon.auth.api.rackspacecloud.com/v1.0" 19 | # config.cloud_files_snet = true 20 | # end 21 | # 22 | # You can optionally include your CDN host name in the configuration. 23 | # This is *highly* recommended, as without it every request requires a lookup 24 | # of this information. 25 | # 26 | # config.cloud_files_cdn_host = "c000000.cdn.rackspacecloud.com" 27 | # 28 | # 29 | class CloudFiles < Abstract 30 | 31 | class File 32 | 33 | def initialize(uploader, base, path) 34 | @uploader = uploader 35 | @path = path 36 | @base = base 37 | end 38 | 39 | ## 40 | # Returns the current path/filename of the file on Cloud Files. 41 | # 42 | # === Returns 43 | # 44 | # [String] A path 45 | # 46 | def path 47 | @path 48 | end 49 | 50 | ## 51 | # Reads the contents of the file from Cloud Files 52 | # 53 | # === Returns 54 | # 55 | # [String] contents of the file 56 | # 57 | def read 58 | object = cf_container.object(@path) 59 | @content_type = object.content_type 60 | object.data 61 | end 62 | 63 | ## 64 | # Remove the file from Cloud Files 65 | # 66 | def delete 67 | begin 68 | cf_container.delete_object(@path) 69 | rescue ::CloudFiles::Exception::NoSuchObject 70 | # If the file's not there, don't panic 71 | nil 72 | end 73 | end 74 | 75 | ## 76 | # Returns the url on the Cloud Files CDN. Note that the parent container must be marked as 77 | # public for this to work. 78 | # 79 | # === Returns 80 | # 81 | # [String] file's url 82 | # 83 | def url 84 | if @uploader.cloud_files_cdn_host 85 | "http://" + @uploader.cloud_files_cdn_host + "/" + @path 86 | else 87 | begin 88 | cf_container.object(@path).public_url 89 | rescue ::CloudFiles::Exception::NoSuchObject 90 | nil 91 | end 92 | end 93 | end 94 | 95 | def content_type 96 | cf_container.object(@path).content_type 97 | end 98 | 99 | def content_type=(new_content_type) 100 | headers["content-type"] = new_content_type 101 | end 102 | 103 | ## 104 | # Writes the supplied data into the object on Cloud Files. 105 | # 106 | # === Returns 107 | # 108 | # boolean 109 | # 110 | def store(data,headers={}) 111 | object = cf_container.create_object(@path) 112 | object.write(data,headers) 113 | end 114 | 115 | private 116 | 117 | def headers 118 | @headers ||= { } 119 | end 120 | 121 | def container 122 | @uploader.cloud_files_container 123 | end 124 | 125 | def connection 126 | @base.connection 127 | end 128 | 129 | def cf_connection 130 | config = {:username => @uploader.cloud_files_username, :api_key => @uploader.cloud_files_api_key} 131 | config[:auth_url] = @uploader.cloud_files_auth_url if @uploader.respond_to?(:cloud_files_auth_url) 132 | config[:snet] = @uploader.cloud_files_snet if @uploader.respond_to?(:cloud_files_snet) 133 | @cf_connection ||= ::CloudFiles::Connection.new(config) 134 | end 135 | 136 | def cf_container 137 | if @cf_container 138 | @cf_container 139 | else 140 | begin 141 | @cf_container = cf_connection.container(container) 142 | rescue NoSuchContainerException 143 | @cf_container = cf_connection.create_container(container) 144 | @cf_container.make_public 145 | end 146 | @cf_container 147 | end 148 | end 149 | 150 | 151 | end 152 | 153 | ## 154 | # Store the file on Cloud Files 155 | # 156 | # === Parameters 157 | # 158 | # [file (CarrierWave::SanitizedFile)] the file to store 159 | # 160 | # === Returns 161 | # 162 | # [CarrierWave::Storage::CloudFiles::File] the stored file 163 | # 164 | def store!(file) 165 | cloud_files_options = {'Content-Type' => file.content_type} 166 | f = CarrierWave::Storage::CloudFiles::File.new(uploader, self, uploader.store_path) 167 | f.store(file.read,cloud_files_options) 168 | f 169 | end 170 | 171 | # Do something to retrieve the file 172 | # 173 | # @param [String] identifier uniquely identifies the file 174 | # 175 | # [identifier (String)] uniquely identifies the file 176 | # 177 | # === Returns 178 | # 179 | # [CarrierWave::Storage::CloudFiles::File] the stored file 180 | # 181 | def retrieve!(identifier) 182 | CarrierWave::Storage::CloudFiles::File.new(uploader, self, uploader.store_path(identifier)) 183 | end 184 | 185 | 186 | end # CloudFiles 187 | end # Storage 188 | end # CarrierWave 189 | -------------------------------------------------------------------------------- /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 :permissions 12 | add_config :storage_engines 13 | add_config :s3_access_policy 14 | add_config :s3_bucket 15 | add_config :s3_access_key_id 16 | add_config :s3_secret_access_key 17 | add_config :s3_cnamed 18 | add_config :s3_headers 19 | add_config :s3_region 20 | add_config :s3_use_ssl 21 | add_config :s3_authentication_timeout 22 | add_config :cloud_files_username 23 | add_config :cloud_files_api_key 24 | add_config :cloud_files_container 25 | add_config :cloud_files_cdn_host 26 | add_config :cloud_files_auth_url 27 | add_config :cloud_files_snet 28 | add_config :grid_fs_connection 29 | add_config :grid_fs_database 30 | add_config :grid_fs_host 31 | add_config :grid_fs_port 32 | add_config :grid_fs_username 33 | add_config :grid_fs_password 34 | add_config :grid_fs_access_url 35 | add_config :store_dir 36 | add_config :cache_dir 37 | add_config :enable_processing 38 | add_config :ensure_multipart_form 39 | add_config :delete_tmp_file_after_storage 40 | add_config :delete_cache_id_after_storage 41 | add_config :remove_previously_stored_files_after_update 42 | 43 | # fog 44 | add_config :fog_attributes 45 | add_config :fog_credentials 46 | add_config :fog_directory 47 | add_config :fog_host 48 | add_config :fog_public 49 | add_config :fog_authenticated_url_expiration 50 | 51 | # Mounting 52 | add_config :ignore_integrity_errors 53 | add_config :ignore_processing_errors 54 | add_config :validate_integrity 55 | add_config :validate_processing 56 | add_config :mount_on 57 | 58 | # set default values 59 | reset_config 60 | end 61 | 62 | module ClassMethods 63 | 64 | ## 65 | # Sets the storage engine to be used when storing files with this uploader. 66 | # Can be any class that implements a #store!(CarrierWave::SanitizedFile) and a #retrieve! 67 | # method. See lib/carrierwave/storage/file.rb for an example. Storage engines should 68 | # be added to CarrierWave::Uploader::Base.storage_engines so they can be referred 69 | # to by a symbol, which should be more convenient 70 | # 71 | # If no argument is given, it will simply return the currently used storage engine. 72 | # 73 | # === Parameters 74 | # 75 | # [storage (Symbol, Class)] The storage engine to use for this uploader 76 | # 77 | # === Returns 78 | # 79 | # [Class] the storage engine to be used with this uploader 80 | # 81 | # === Examples 82 | # 83 | # storage :file 84 | # storage CarrierWave::Storage::File 85 | # storage MyCustomStorageEngine 86 | # 87 | def storage(storage = nil) 88 | if storage 89 | self._storage = storage.is_a?(Symbol) ? eval(storage_engines[storage]) : storage 90 | end 91 | _storage 92 | end 93 | alias_method :storage=, :storage 94 | 95 | def add_config(name) 96 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 97 | def self.#{name}(value=nil) 98 | @#{name} = value if value 99 | return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name}) 100 | name = superclass.#{name} 101 | return nil if name.nil? && !instance_variable_defined?("@#{name}") 102 | @#{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 103 | end 104 | 105 | def self.#{name}=(value) 106 | @#{name} = value 107 | end 108 | 109 | def #{name} 110 | self.class.#{name} 111 | end 112 | RUBY 113 | end 114 | 115 | def configure 116 | yield self 117 | end 118 | 119 | ## 120 | # sets configuration back to default 121 | # 122 | def reset_config 123 | configure do |config| 124 | config.permissions = 0644 125 | config.storage_engines = { 126 | :file => "CarrierWave::Storage::File", 127 | :fog => "CarrierWave::Storage::Fog", 128 | :s3 => "CarrierWave::Storage::S3", 129 | :grid_fs => "CarrierWave::Storage::GridFS", 130 | :right_s3 => "CarrierWave::Storage::RightS3", 131 | :cloud_files => "CarrierWave::Storage::CloudFiles" 132 | } 133 | config.storage = :file 134 | config.s3_headers = {} 135 | config.s3_access_policy = :public_read 136 | config.s3_region = 'us-east-1' 137 | config.s3_authentication_timeout = 600 138 | config.grid_fs_database = 'carrierwave' 139 | config.grid_fs_host = 'localhost' 140 | config.grid_fs_port = 27017 141 | config.fog_attributes = {} 142 | config.fog_credentials = {} 143 | config.fog_public = true 144 | config.fog_authenticated_url_expiration = 600 145 | config.store_dir = 'uploads' 146 | config.cache_dir = 'uploads/tmp' 147 | config.delete_tmp_file_after_storage = true 148 | config.delete_cache_id_after_storage = true 149 | config.remove_previously_stored_files_after_update = true 150 | config.ignore_integrity_errors = true 151 | config.ignore_processing_errors = true 152 | config.validate_integrity = true 153 | config.validate_processing = true 154 | config.root = CarrierWave.root 155 | config.enable_processing = true 156 | config.ensure_multipart_form = true 157 | end 158 | end 159 | end 160 | 161 | end 162 | end 163 | end 164 | 165 | -------------------------------------------------------------------------------- /spec/storage/fog_helper.rb: -------------------------------------------------------------------------------- 1 | def fog_tests(fog_credentials) 2 | describe CarrierWave::Storage::Fog do 3 | describe fog_credentials[:provider] do 4 | before do 5 | CarrierWave.configure do |config| 6 | config.reset_config 7 | config.fog_attributes = {} 8 | config.fog_credentials = fog_credentials 9 | config.fog_directory = CARRIERWAVE_DIRECTORY 10 | config.fog_host = nil 11 | config.fog_public = true 12 | end 13 | 14 | eval <<-RUBY 15 | class FogSpec#{fog_credentials[:provider]}Uploader < CarrierWave::Uploader::Base 16 | storage :fog 17 | end 18 | RUBY 19 | 20 | @provider = fog_credentials[:provider] 21 | 22 | # @uploader = FogSpecUploader.new 23 | @uploader = eval("FogSpec#{@provider}Uploader") 24 | @uploader.stub!(:store_path).and_return('uploads/test.jpg') 25 | 26 | @storage = CarrierWave::Storage::Fog.new(@uploader) 27 | @directory = @storage.connection.directories.get(CARRIERWAVE_DIRECTORY) || @storage.connection.directories.create(:key => CARRIERWAVE_DIRECTORY) 28 | 29 | @file = CarrierWave::SanitizedFile.new( 30 | :tempfile => StringIO.new(File.open(file_path('test.jpg')).read), 31 | :filename => 'test.jpg', 32 | :content_type => 'image/jpeg' 33 | ) 34 | end 35 | 36 | describe '#store!' do 37 | before do 38 | @uploader.stub!(:store_path).and_return('uploads/test.jpg') 39 | @fog_file = @storage.store!(@file) 40 | end 41 | 42 | it "should upload the file" do 43 | @directory.files.get('uploads/test.jpg').body.should == 'this is stuff' 44 | end 45 | 46 | it "should have a path" do 47 | @fog_file.path.should == 'uploads/test.jpg' 48 | end 49 | 50 | it "should have a content_type" do 51 | @fog_file.content_type.should == 'image/jpeg' 52 | @directory.files.get('uploads/test.jpg').content_type.should == 'image/jpeg' 53 | end 54 | 55 | context "without fog_host" do 56 | it "should have a public_url" do 57 | unless fog_credentials[:provider] == 'Local' 58 | @fog_file.public_url.should_not be_nil 59 | end 60 | end 61 | 62 | it "should have a url" do 63 | unless fog_credentials[:provider] == 'Local' 64 | @fog_file.url.should_not be_nil 65 | end 66 | end 67 | end 68 | 69 | context "with fog_host" do 70 | it "should have a fog_host rooted public_url" do 71 | @uploader.stub!(:fog_host).and_return('http://foo.bar') 72 | @fog_file.public_url.should == 'http://foo.bar/uploads/test.jpg' 73 | end 74 | 75 | it "should have a fog_host rooted url" do 76 | @uploader.stub!(:fog_host).and_return('http://foo.bar') 77 | @fog_file.url.should == 'http://foo.bar/uploads/test.jpg' 78 | end 79 | 80 | it "should always have the same fog_host rooted url" do 81 | @uploader.stub!(:fog_host).and_return('http://foo.bar') 82 | @fog_file.url.should == 'http://foo.bar/uploads/test.jpg' 83 | @fog_file.url.should == 'http://foo.bar/uploads/test.jpg' 84 | end 85 | end 86 | 87 | it "should return filesize" do 88 | @fog_file.size.should == 13 89 | end 90 | 91 | it "should be deletable" do 92 | @fog_file.delete 93 | @directory.files.head('uploads/test.jpg').should == nil 94 | end 95 | end 96 | 97 | describe '#cache! and then #store!' do 98 | before do 99 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 100 | @uploader_instance = @uploader.new 101 | @uploader_instance.cache!(File.open(file_path('test.jpg'))) 102 | @uploader_instance.retrieve_from_cache!('20071201-1234-345-2255/test.jpg') 103 | @file = @uploader_instance.file 104 | @storage.store!(@file) 105 | end 106 | 107 | it "should have a content_type" do 108 | @uploader_instance.file.content_type.should == 'image/jpeg' 109 | @directory.files.get('uploads/test.jpg').content_type.should == 'image/jpeg' 110 | end 111 | end 112 | 113 | describe '#retrieve!' do 114 | before do 115 | @directory.files.create(:key => 'uploads/test.jpg', :body => 'A test, 1234', :public => true) 116 | @uploader.stub!(:store_path).with('test.jpg').and_return('uploads/test.jpg') 117 | @fog_file = @storage.retrieve!('test.jpg') 118 | end 119 | 120 | it "should retrieve the file contents" do 121 | @fog_file.read.chomp.should == "A test, 1234" 122 | end 123 | 124 | it "should have a path" do 125 | @fog_file.path.should == 'uploads/test.jpg' 126 | end 127 | 128 | it "should have a public url" do 129 | unless fog_credentials[:provider] == 'Local' 130 | @fog_file.public_url.should_not be_nil 131 | end 132 | end 133 | 134 | it "should return filesize" do 135 | @fog_file.size.should == 12 136 | end 137 | 138 | it "should be deletable" do 139 | @fog_file.delete 140 | @directory.files.head('uploads/test.jpg').should == nil 141 | end 142 | end 143 | 144 | describe 'fog_public' do 145 | 146 | context "true" do 147 | before do 148 | directory_key = "#{CARRIERWAVE_DIRECTORY}public" 149 | @directory = @storage.connection.directories.create(:key => directory_key) 150 | @uploader.stub!(:fog_directory).and_return(directory_key) 151 | @uploader.stub!(:store_path).and_return('uploads/public.txt') 152 | @fog_file = @storage.store!(@file) 153 | end 154 | 155 | after do 156 | @directory.files.new(:key => 'uploads/public.txt').destroy 157 | @directory.destroy 158 | end 159 | 160 | it "should be available at public URL" do 161 | unless Fog.mocking? || fog_credentials[:provider] == 'Local' 162 | open(@fog_file.public_url).read.should == 'this is stuff' 163 | end 164 | end 165 | end 166 | 167 | context "false" do 168 | before do 169 | directory_key = "#{CARRIERWAVE_DIRECTORY}private" 170 | @directory = @storage.connection.directories.create(:key => directory_key) 171 | @uploader.stub!(:fog_directory).and_return(directory_key) 172 | @uploader.stub!(:fog_public).and_return(false) 173 | @uploader.stub!(:store_path).and_return('uploads/private.txt') 174 | @fog_file = @storage.store!(@file) 175 | end 176 | 177 | after do 178 | @directory.files.new(:key => 'uploads/private.txt').destroy 179 | @directory.destroy 180 | end 181 | 182 | it "should have an authenticated_url" do 183 | if ['AWS', 'Google'].include?(@provider) 184 | @fog_file.authenticated_url.should_not be_nil 185 | end 186 | end 187 | end 188 | end 189 | 190 | context 'finishing' do 191 | it "should destroy the directory" do # hack, but after never does what/when I want 192 | @directory.destroy 193 | end 194 | end 195 | 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/s3.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | begin 3 | require 'fog' 4 | rescue LoadError 5 | raise "You don't have the 'fog' gem installed. The 'aws', 'aws-s3' and 'right_aws' gems are no longer supported." 6 | end 7 | 8 | module CarrierWave 9 | module Storage 10 | 11 | ## 12 | # Uploads things to Amazon S3 using the "fog" gem. 13 | # You'll need to specify the access_key_id, secret_access_key and bucket. 14 | # 15 | # CarrierWave.configure do |config| 16 | # config.s3_access_key_id = "xxxxxx" 17 | # config.s3_secret_access_key = "xxxxxx" 18 | # config.s3_bucket = "my_bucket_name" 19 | # end 20 | # 21 | # You can set the access policy for the uploaded files: 22 | # 23 | # CarrierWave.configure do |config| 24 | # config.s3_access_policy = :public_read 25 | # end 26 | # 27 | # The default is :public_read. For more options see: 28 | # 29 | # http://docs.amazonwebservices.com/AmazonS3/latest/RESTAccessPolicy.html#RESTCannedAccessPolicies 30 | # 31 | # The following access policies are available: 32 | # 33 | # [:private] No one else has any access rights. 34 | # [:public_read] The anonymous principal is granted READ access. 35 | # If this policy is used on an object, it can be read from a 36 | # browser with no authentication. 37 | # [:public_read_write] The anonymous principal is granted READ and WRITE access. 38 | # [:authenticated_read] Any principal authenticated as a registered Amazon S3 user 39 | # is granted READ access. 40 | # 41 | # You can change the generated url to a cnamed domain by setting the cnamed config: 42 | # 43 | # CarrierWave.configure do |config| 44 | # config.s3_cnamed = true 45 | # config.s3_bucket = 'bucketname.domain.tld' 46 | # end 47 | # 48 | # Now the resulting url will be 49 | # 50 | # http://bucketname.domain.tld/path/to/file 51 | # 52 | # instead of 53 | # 54 | # http://bucketname.domain.tld.s3.amazonaws.com/path/to/file 55 | # 56 | # You can specify a region. US Standard "us-east-1" is the default. 57 | # 58 | # CarrierWave.configure do |config| 59 | # config.s3_region = 'eu-west-1' 60 | # end 61 | # 62 | # Available options are defined in Fog Storage[http://github.com/geemus/fog/blob/master/lib/fog/aws/storage.rb] 63 | # 64 | # 'eu-west-1' => 's3-eu-west-1.amazonaws.com' 65 | # 'us-east-1' => 's3.amazonaws.com' 66 | # 'ap-southeast-1' => 's3-ap-southeast-1.amazonaws.com' 67 | # 'us-west-1' => 's3-us-west-1.amazonaws.com' 68 | # 69 | class S3 < Abstract 70 | 71 | class File 72 | 73 | def initialize(uploader, base, path) 74 | @uploader = uploader 75 | @path = path 76 | @base = base 77 | end 78 | 79 | ## 80 | # Returns the current path of the file on S3 81 | # 82 | # === Returns 83 | # 84 | # [String] A path 85 | # 86 | def path 87 | @path 88 | end 89 | 90 | ## 91 | # Reads the contents of the file from S3 92 | # 93 | # === Returns 94 | # 95 | # [String] contents of the file 96 | # 97 | def read 98 | result = connection.get_object(bucket, @path) 99 | @headers = result.headers 100 | result.body 101 | end 102 | 103 | ## 104 | # Remove the file from Amazon S3 105 | # 106 | def delete 107 | connection.delete_object(bucket, @path) 108 | end 109 | 110 | ## 111 | # Returns the url on Amazon's S3 service 112 | # 113 | # === Returns 114 | # 115 | # [String] file's url 116 | # 117 | def url 118 | if access_policy == :authenticated_read 119 | authenticated_url 120 | else 121 | public_url 122 | end 123 | end 124 | 125 | def public_url 126 | scheme = use_ssl? ? 'https' : 'http' 127 | if cnamed? 128 | ["#{scheme}://#{bucket}", path].compact.join('/') 129 | else 130 | ["#{scheme}://#{bucket}.s3.amazonaws.com", path].compact.join('/') 131 | end 132 | end 133 | 134 | def authenticated_url 135 | connection.get_object_url(bucket, path, Time.now + authentication_timeout) 136 | end 137 | 138 | def store(file) 139 | content_type ||= file.content_type # this might cause problems if content type changes between read and upload (unlikely) 140 | connection.put_object(bucket, path, file.read, 141 | { 142 | 'x-amz-acl' => access_policy.to_s.gsub('_', '-'), 143 | 'Content-Type' => content_type 144 | }.merge(@uploader.s3_headers) 145 | ) 146 | end 147 | 148 | def content_type 149 | headers["Content-Type"] 150 | end 151 | 152 | def content_type=(type) 153 | headers["Content-Type"] = type 154 | end 155 | 156 | def size 157 | headers['Content-Length'].to_i 158 | end 159 | 160 | # Headers returned from file retrieval 161 | def headers 162 | @headers ||= begin 163 | connection.head_object(bucket, @path).headers 164 | rescue Excon::Errors::NotFound # Don't die, just return no headers 165 | {} 166 | end 167 | end 168 | 169 | private 170 | 171 | def use_ssl? 172 | @uploader.s3_use_ssl 173 | end 174 | 175 | def cnamed? 176 | @uploader.s3_cnamed 177 | end 178 | 179 | def access_policy 180 | @uploader.s3_access_policy 181 | end 182 | 183 | def bucket 184 | @uploader.s3_bucket 185 | end 186 | 187 | def authentication_timeout 188 | @uploader.s3_authentication_timeout 189 | end 190 | 191 | def connection 192 | @base.connection 193 | end 194 | 195 | end 196 | 197 | ## 198 | # Store the file on S3 199 | # 200 | # === Parameters 201 | # 202 | # [file (CarrierWave::SanitizedFile)] the file to store 203 | # 204 | # === Returns 205 | # 206 | # [CarrierWave::Storage::S3::File] the stored file 207 | # 208 | def store!(file) 209 | f = CarrierWave::Storage::S3::File.new(uploader, self, uploader.store_path) 210 | f.store(file) 211 | f 212 | end 213 | 214 | # Do something to retrieve the file 215 | # 216 | # @param [String] identifier uniquely identifies the file 217 | # 218 | # [identifier (String)] uniquely identifies the file 219 | # 220 | # === Returns 221 | # 222 | # [CarrierWave::Storage::S3::File] the stored file 223 | # 224 | def retrieve!(identifier) 225 | CarrierWave::Storage::S3::File.new(uploader, self, uploader.store_path(identifier)) 226 | end 227 | 228 | def connection 229 | @connection ||= ::Fog::Storage.new( 230 | :aws_access_key_id => uploader.s3_access_key_id, 231 | :aws_secret_access_key => uploader.s3_secret_access_key, 232 | :provider => 'AWS', 233 | :region => uploader.s3_region 234 | ) 235 | end 236 | 237 | end # S3 238 | end # Storage 239 | end # CarrierWave 240 | 241 | -------------------------------------------------------------------------------- /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 | ## 12 | # Add configuration options for versions 13 | # class_inheritable_accessor was deprecated in Rails 3.1 and removed for 3.2. 14 | # class_attribute was added in 3.0, but doesn't support omitting the instance_reader until 3.0.10 15 | # For max compatibility, always use class_inheritable_accessor when possible 16 | if respond_to?(:class_inheritable_accessor) 17 | ActiveSupport::Deprecation.silence do 18 | class_inheritable_accessor :versions, :version_names, :instance_reader => false, :instance_writer => false 19 | end 20 | else 21 | class_attribute :versions, :version_names, :instance_reader => false, :instance_writer => false 22 | end 23 | 24 | self.versions = {} 25 | self.version_names = [] 26 | 27 | after :cache, :cache_versions! 28 | after :store, :store_versions! 29 | after :remove, :remove_versions! 30 | after :retrieve_from_cache, :retrieve_versions_from_cache! 31 | after :retrieve_from_store, :retrieve_versions_from_store! 32 | end 33 | 34 | module ClassMethods 35 | 36 | ## 37 | # Adds a new version to this uploader 38 | # 39 | # === Parameters 40 | # 41 | # [name (#to_sym)] name of the version 42 | # [options (Hash)] optional options hash 43 | # [&block (Proc)] a block to eval on this version of the uploader 44 | # 45 | # === Examples 46 | # 47 | # class MyUploader < CarrierWave::Uploader::Base 48 | # 49 | # version :thumb do 50 | # process :scale => [200, 200] 51 | # end 52 | # 53 | # version :preview, :if => :image? do 54 | # process :scale => [200, 200] 55 | # end 56 | # 57 | # end 58 | # 59 | def version(name, options = {}, &block) 60 | name = name.to_sym 61 | unless versions[name] 62 | uploader = Class.new(self) 63 | uploader.versions = {} 64 | 65 | # Define the enable_processing method for versions so they get the 66 | # value from the parent class unless explicitly overwritten 67 | uploader.class_eval <<-RUBY, __FILE__, __LINE__ + 1 68 | def self.enable_processing(value=nil) 69 | self.enable_processing = value if value 70 | if !@enable_processing.nil? 71 | @enable_processing 72 | else 73 | superclass.enable_processing 74 | end 75 | end 76 | RUBY 77 | 78 | # Add the current version hash to class attribute :versions 79 | current_version = {} 80 | current_version[name] = { 81 | :uploader => uploader, 82 | :options => options 83 | } 84 | self.versions = versions.merge(current_version) 85 | 86 | versions[name][:uploader].version_names += [name] 87 | 88 | class_eval <<-RUBY 89 | def #{name} 90 | versions[:#{name}] 91 | end 92 | RUBY 93 | # as the processors get the output from the previous processors as their 94 | # input we must not stack the processors here 95 | versions[name][:uploader].processors = versions[name][:uploader].processors.dup 96 | versions[name][:uploader].processors.clear 97 | end 98 | versions[name][:uploader].class_eval(&block) if block 99 | versions[name] 100 | end 101 | 102 | def recursively_apply_block_to_versions(&block) 103 | versions.each do |name, version| 104 | version[:uploader].class_eval(&block) 105 | version[:uploader].recursively_apply_block_to_versions(&block) 106 | end 107 | end 108 | end # ClassMethods 109 | 110 | ## 111 | # Returns a hash mapping the name of each version of the uploader to an instance of it 112 | # 113 | # === Returns 114 | # 115 | # [Hash{Symbol => CarrierWave::Uploader}] a list of uploader instances 116 | # 117 | def versions 118 | return @versions if @versions 119 | @versions = {} 120 | self.class.versions.each do |name, version| 121 | @versions[name] = version[:uploader].new(model, mounted_as) 122 | end 123 | @versions 124 | end 125 | 126 | ## 127 | # === Returns 128 | # 129 | # [String] the name of this version of the uploader 130 | # 131 | def version_name 132 | self.class.version_names.join('_').to_sym unless self.class.version_names.blank? 133 | end 134 | 135 | ## 136 | # When given a version name as a parameter, will return the url for that version 137 | # This also works with nested versions. 138 | # 139 | # === Example 140 | # 141 | # my_uploader.url # => /path/to/my/uploader.gif 142 | # my_uploader.url(:thumb) # => /path/to/my/thumb_uploader.gif 143 | # my_uploader.url(:thumb, :small) # => /path/to/my/thumb_small_uploader.gif 144 | # 145 | # === Parameters 146 | # 147 | # [*args (Symbol)] any number of versions 148 | # 149 | # === Returns 150 | # 151 | # [String] the location where this file is accessible via a url 152 | # 153 | def url(*args) 154 | if(args.first) 155 | raise ArgumentError, "Version #{args.first} doesn't exist!" if versions[args.first.to_sym].nil? 156 | # recursively proxy to version 157 | versions[args.first.to_sym].url(*args[1..-1]) 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! 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 | cache_stored_file! if !cached? 175 | 176 | store! 177 | end 178 | 179 | private 180 | 181 | def active_versions 182 | versions.select do |name, uploader| 183 | condition = self.class.versions[name][:options][:if] 184 | not condition or send(condition, file) 185 | end 186 | end 187 | 188 | def full_filename(for_file) 189 | [version_name, super(for_file)].compact.join('_') 190 | end 191 | 192 | def full_original_filename 193 | [version_name, super].compact.join('_') 194 | end 195 | 196 | def cache_versions!(new_file) 197 | # We might have processed the new_file argument after the callbacks were 198 | # initialized, so get the actual file based off of the current state of 199 | # our file 200 | processed_parent = SanitizedFile.new :tempfile => self.file, 201 | :filename => new_file.original_filename 202 | 203 | active_versions.each do |name, v| 204 | v.send(:cache_id=, cache_id) 205 | v.cache!(processed_parent) 206 | end 207 | end 208 | 209 | def store_versions!(new_file) 210 | active_versions.each { |name, v| v.store!(new_file) } 211 | end 212 | 213 | def remove_versions! 214 | versions.each { |name, v| v.remove! } 215 | end 216 | 217 | def retrieve_versions_from_cache!(cache_name) 218 | versions.each { |name, v| v.retrieve_from_cache!(cache_name) } 219 | end 220 | 221 | def retrieve_versions_from_store!(identifier) 222 | versions.each { |name, v| v.retrieve_from_store!(identifier) } 223 | end 224 | 225 | end # Versions 226 | end # Uploader 227 | end # CarrierWave 228 | -------------------------------------------------------------------------------- /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? 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 | # 172 | def move_to(new_path, permissions=nil) 173 | return if self.empty? 174 | new_path = File.expand_path(new_path) 175 | 176 | mkdir!(new_path) 177 | if exists? 178 | FileUtils.mv(path, new_path) unless new_path == path 179 | else 180 | File.open(new_path, "wb") { |f| f.write(read) } 181 | end 182 | chmod!(new_path, permissions) 183 | self.file = new_path 184 | end 185 | 186 | ## 187 | # Creates a copy of this file and moves it to the given path. Returns the copy. 188 | # 189 | # === Parameters 190 | # 191 | # [new_path (String)] The path where the file should be copied to. 192 | # [permissions (Integer)] permissions to set on the copy 193 | # 194 | # === Returns 195 | # 196 | # @return [CarrierWave::SanitizedFile] the location where the file will be stored. 197 | # 198 | def copy_to(new_path, permissions=nil) 199 | return if self.empty? 200 | new_path = File.expand_path(new_path) 201 | 202 | mkdir!(new_path) 203 | if exists? 204 | FileUtils.cp(path, new_path) unless new_path == path 205 | else 206 | File.open(new_path, "wb") { |f| f.write(read) } 207 | end 208 | chmod!(new_path, permissions) 209 | self.class.new({:tempfile => new_path, :content_type => content_type}) 210 | end 211 | 212 | ## 213 | # Removes the file from the filesystem. 214 | # 215 | def delete 216 | FileUtils.rm(self.path) if exists? 217 | end 218 | 219 | ## 220 | # Returns the content type of the file. 221 | # 222 | # === Returns 223 | # 224 | # [String] the content type of the file 225 | # 226 | def content_type 227 | return @content_type if @content_type 228 | @file.content_type.chomp if @file.respond_to?(:content_type) and @file.content_type 229 | end 230 | 231 | ## 232 | # Sets the content type of the file. 233 | # 234 | # === Returns 235 | # 236 | # [String] the content type of the file 237 | # 238 | def content_type=(type) 239 | @content_type = type 240 | end 241 | 242 | ## 243 | # Used to sanitize the file name. Public to allow overriding for non-latin characters. 244 | # 245 | # === Returns 246 | # 247 | # [Regexp] the regexp for sanitizing the file name 248 | # 249 | def sanitize_regexp 250 | CarrierWave::SanitizedFile.sanitize_regexp 251 | end 252 | 253 | private 254 | 255 | def file=(file) 256 | if file.is_a?(Hash) 257 | @file = file["tempfile"] || file[:tempfile] 258 | @original_filename = file["filename"] || file[:filename] 259 | @content_type = file["content_type"] || file[:content_type] 260 | else 261 | @file = file 262 | @original_filename = nil 263 | @content_type = nil 264 | end 265 | end 266 | 267 | # create the directory if it doesn't exist 268 | def mkdir!(path) 269 | FileUtils.mkdir_p(File.dirname(path)) unless File.exists?(File.dirname(path)) 270 | end 271 | 272 | def chmod!(path, permissions) 273 | File.chmod(permissions, path) if permissions 274 | end 275 | 276 | # Sanitize the filename, to prevent hacking 277 | def sanitize(name) 278 | name = name.gsub("\\", "/") # work-around for IE 279 | name = File.basename(name) 280 | name = name.gsub(sanitize_regexp,"_") 281 | name = "_#{name}" if name =~ /\A\.+\z/ 282 | name = "unnamed" if name.size == 0 283 | return name.mb_chars.to_s 284 | end 285 | 286 | def split_extension(filename) 287 | # regular expressions to try for identifying extensions 288 | extension_matchers = [ 289 | /\A(.+)\.(tar\.gz)\z/, # matches "something.tar.gz" 290 | /\A(.+)\.([^\.]+)\z/ # matches "something.jpg" 291 | ] 292 | 293 | extension_matchers.each do |regexp| 294 | if filename =~ regexp 295 | return $1, $2 296 | end 297 | end 298 | return filename, "" # In case we weren't able to split the extension 299 | end 300 | 301 | end # SanitizedFile 302 | end # CarrierWave 303 | -------------------------------------------------------------------------------- /lib/carrierwave/processing/mini_magick.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'mini_magick' 4 | 5 | module CarrierWave 6 | 7 | ## 8 | # This module simplifies manipulation with MiniMagick by providing a set 9 | # of convenient helper methods. If you want to use them, you'll need to 10 | # require this file: 11 | # 12 | # require 'carrierwave/processing/mini_magick' 13 | # 14 | # And then include it in your uploader: 15 | # 16 | # class MyUploader < CarrierWave::Uploader::Base 17 | # include CarrierWave::MiniMagick 18 | # end 19 | # 20 | # You can now use the provided helpers: 21 | # 22 | # class MyUploader < CarrierWave::Uploader::Base 23 | # include CarrierWave::MiniMagick 24 | # 25 | # process :resize_to_fit => [200, 200] 26 | # end 27 | # 28 | # Or create your own helpers with the powerful manipulate! method. Check 29 | # out the ImageMagick docs at http://www.imagemagick.org/script/command-line-options.php for more 30 | # info 31 | # 32 | # class MyUploader < CarrierWave::Uploader::Base 33 | # include CarrierWave::MiniMagick 34 | # 35 | # process :do_stuff => 10.0 36 | # 37 | # def do_stuff(blur_factor) 38 | # manipulate! do |img| 39 | # img = img.sepiatone 40 | # img = img.auto_orient 41 | # img = img.radial_blur blur_factor 42 | # end 43 | # end 44 | # end 45 | # 46 | # === Note 47 | # 48 | # MiniMagick is a mini replacement for RMagick that uses the command line 49 | # tool "mogrify" for image manipulation. 50 | # 51 | # You can find more information here: 52 | # 53 | # http://mini_magick.rubyforge.org/ 54 | # and 55 | # http://github.com/probablycorey/mini_magick/ 56 | # 57 | # 58 | module MiniMagick 59 | extend ActiveSupport::Concern 60 | 61 | module ClassMethods 62 | def convert(format) 63 | process :convert => format 64 | end 65 | 66 | def resize_to_limit(width, height) 67 | process :resize_to_limit => [width, height] 68 | end 69 | 70 | def resize_to_fit(width, height) 71 | process :resize_to_fit => [width, height] 72 | end 73 | 74 | def resize_to_fill(width, height) 75 | process :resize_to_fill => [width, height] 76 | end 77 | 78 | def resize_and_pad(width, height, background=:transparent, gravity=::Magick::CenterGravity) 79 | process :resize_and_pad => [width, height, background, gravity] 80 | end 81 | end 82 | 83 | ## 84 | # Changes the image encoding format to the given format 85 | # 86 | # See http://www.imagemagick.org/script/command-line-options.php#format 87 | # 88 | # === Parameters 89 | # 90 | # [format (#to_s)] an abreviation of the format 91 | # 92 | # === Yields 93 | # 94 | # [MiniMagick::Image] additional manipulations to perform 95 | # 96 | # === Examples 97 | # 98 | # image.convert(:png) 99 | # 100 | def convert(format) 101 | manipulate! do |img| 102 | img.format(format.to_s.downcase) 103 | img = yield(img) if block_given? 104 | img 105 | end 106 | end 107 | 108 | ## 109 | # Resize the image to fit within the specified dimensions while retaining 110 | # the original aspect ratio. Will only resize the image if it is larger than the 111 | # specified dimensions. The resulting image may be shorter or narrower than specified 112 | # in the smaller dimension but will not be larger than the specified values. 113 | # 114 | # === Parameters 115 | # 116 | # [width (Integer)] the width to scale the image to 117 | # [height (Integer)] the height to scale the image to 118 | # 119 | # === Yields 120 | # 121 | # [MiniMagick::Image] additional manipulations to perform 122 | # 123 | def resize_to_limit(width, height) 124 | manipulate! do |img| 125 | img.resize "#{width}x#{height}>" 126 | img = yield(img) if block_given? 127 | img 128 | end 129 | end 130 | 131 | ## 132 | # Resize the image to fit within the specified dimensions while retaining 133 | # the original aspect ratio. The image may be shorter or narrower than 134 | # specified in the smaller dimension but will not be larger than the specified values. 135 | # 136 | # === Parameters 137 | # 138 | # [width (Integer)] the width to scale the image to 139 | # [height (Integer)] the height to scale the image to 140 | # 141 | # === Yields 142 | # 143 | # [MiniMagick::Image] additional manipulations to perform 144 | # 145 | def resize_to_fit(width, height) 146 | manipulate! do |img| 147 | img.resize "#{width}x#{height}" 148 | img = yield(img) if block_given? 149 | img 150 | end 151 | end 152 | 153 | ## 154 | # Resize the image to fit within the specified dimensions while retaining 155 | # the aspect ratio of the original image. If necessary, crop the image in the 156 | # larger dimension. 157 | # 158 | # === Parameters 159 | # 160 | # [width (Integer)] the width to scale the image to 161 | # [height (Integer)] the height to scale the image to 162 | # 163 | # === Yields 164 | # 165 | # [MiniMagick::Image] additional manipulations to perform 166 | # 167 | def resize_to_fill(width, height, gravity = 'Center') 168 | manipulate! do |img| 169 | cols, rows = img[:dimensions] 170 | img.combine_options do |cmd| 171 | if width != cols || height != rows 172 | scale = [width/cols.to_f, height/rows.to_f].max 173 | cols = (scale * (cols + 0.5)).round 174 | rows = (scale * (rows + 0.5)).round 175 | cmd.resize "#{cols}x#{rows}" 176 | end 177 | cmd.gravity gravity 178 | cmd.extent "#{width}x#{height}" if cols != width || rows != height 179 | end 180 | img = yield(img) if block_given? 181 | img 182 | end 183 | end 184 | 185 | ## 186 | # Resize the image to fit within the specified dimensions while retaining 187 | # the original aspect ratio. If necessary, will pad the remaining area 188 | # with the given color, which defaults to transparent (for gif and png, 189 | # white for jpeg). 190 | # 191 | # See http://www.imagemagick.org/script/command-line-options.php#gravity 192 | # for gravity options. 193 | # 194 | # === Parameters 195 | # 196 | # [width (Integer)] the width to scale the image to 197 | # [height (Integer)] the height to scale the image to 198 | # [background (String, :transparent)] the color of the background as a hexcode, like "#ff45de" 199 | # [gravity (String)] how to position the image 200 | # 201 | # === Yields 202 | # 203 | # [MiniMagick::Image] additional manipulations to perform 204 | # 205 | def resize_and_pad(width, height, background=:transparent, gravity='Center') 206 | manipulate! do |img| 207 | img.combine_options do |cmd| 208 | cmd.thumbnail "#{width}x#{height}>" 209 | if background == :transparent 210 | cmd.background "rgba(0, 0, 0, 0.0)" 211 | else 212 | cmd.background background 213 | end 214 | cmd.gravity gravity 215 | cmd.extent "#{width}x#{height}" 216 | end 217 | img = yield(img) if block_given? 218 | img 219 | end 220 | end 221 | 222 | ## 223 | # Manipulate the image with MiniMagick. This method will load up an image 224 | # and then pass each of its frames to the supplied block. It will then 225 | # save the image to disk. 226 | # 227 | # === Gotcha 228 | # 229 | # This method assumes that the object responds to +current_path+. 230 | # Any class that this module is mixed into must have a +current_path+ method. 231 | # CarrierWave::Uploader does, so you won't need to worry about this in 232 | # most cases. 233 | # 234 | # === Yields 235 | # 236 | # [MiniMagick::Image] manipulations to perform 237 | # 238 | # === Raises 239 | # 240 | # [CarrierWave::ProcessingError] if manipulation failed. 241 | # 242 | def manipulate! 243 | cache_stored_file! if !cached? 244 | image = ::MiniMagick::Image.open(current_path) 245 | image = yield(image) 246 | image.write(current_path) 247 | ::MiniMagick::Image.open(current_path) 248 | rescue ::MiniMagick::Error, ::MiniMagick::Invalid => e 249 | raise CarrierWave::ProcessingError.new("Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: #{e}") 250 | end 251 | 252 | end # MiniMagick 253 | end # CarrierWave 254 | -------------------------------------------------------------------------------- /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 | @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 '.clean_cached_files!' do 17 | before do 18 | @cache_dir = File.expand_path(@uploader_class.cache_dir, CarrierWave.root) 19 | FileUtils.mkdir_p File.expand_path('20071201-1234-234-2213', @cache_dir) 20 | FileUtils.mkdir_p File.expand_path('20071203-1234-234-2213', @cache_dir) 21 | FileUtils.mkdir_p File.expand_path('20071205-1234-234-2213', @cache_dir) 22 | end 23 | 24 | after { FileUtils.rm_rf(@cache_dir) } 25 | 26 | it "should clear all files older than, by defaul, 24 hours in the default cache directory" do 27 | Timecop.freeze(Time.utc(2007, 12, 6, 10, 12)) do 28 | @uploader_class.clean_cached_files! 29 | end 30 | Dir.glob("#{@cache_dir}/*").size.should == 1 31 | end 32 | 33 | it "should permit to set since how many seconds delete the cached files" do 34 | Timecop.freeze(Time.utc(2007, 12, 6, 10, 12)) do 35 | @uploader_class.clean_cached_files!(60*60*24*4) 36 | end 37 | Dir.glob("#{@cache_dir}/*").should have(2).element 38 | end 39 | 40 | it "should be aliased on the CarrierWave module" do 41 | Timecop.freeze(Time.utc(2007, 12, 6, 10, 12)) do 42 | CarrierWave.clean_cached_files! 43 | end 44 | Dir.glob("#{@cache_dir}/*").size.should == 1 45 | end 46 | end 47 | 48 | describe '#cache_dir' do 49 | it "should default to the config option" do 50 | @uploader.cache_dir.should == 'uploads/tmp' 51 | end 52 | end 53 | 54 | describe '#cache!' do 55 | 56 | before do 57 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 58 | end 59 | 60 | it "should cache a file" do 61 | @uploader.cache!(File.open(file_path('test.jpg'))) 62 | @uploader.file.should be_an_instance_of(CarrierWave::SanitizedFile) 63 | end 64 | 65 | it "should be cached" do 66 | @uploader.cache!(File.open(file_path('test.jpg'))) 67 | @uploader.should be_cached 68 | end 69 | 70 | it "should store the cache name" do 71 | @uploader.cache!(File.open(file_path('test.jpg'))) 72 | @uploader.cache_name.should == '20071201-1234-345-2255/test.jpg' 73 | end 74 | 75 | it "should set the filename to the file's sanitized filename" do 76 | @uploader.cache!(File.open(file_path('test.jpg'))) 77 | @uploader.filename.should == 'test.jpg' 78 | end 79 | 80 | it "should move it to the tmp dir" do 81 | @uploader.cache!(File.open(file_path('test.jpg'))) 82 | @uploader.file.path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 83 | @uploader.file.exists?.should be_true 84 | end 85 | 86 | it "should set the url" do 87 | @uploader.cache!(File.open(file_path('test.jpg'))) 88 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpg' 89 | end 90 | 91 | it "should raise an error when trying to cache a string" do 92 | running { 93 | @uploader.cache!(file_path('test.jpg')) 94 | }.should raise_error(CarrierWave::FormNotMultipart) 95 | end 96 | 97 | it "should raise an error when trying to cache a pathname" do 98 | running { 99 | @uploader.cache!(Pathname.new(file_path('test.jpg'))) 100 | }.should raise_error(CarrierWave::FormNotMultipart) 101 | end 102 | 103 | it "should do nothing when trying to cache an empty file" do 104 | @uploader.cache!(nil) 105 | end 106 | 107 | it "should set permissions if options are given" do 108 | @uploader_class.permissions = 0777 109 | 110 | @uploader.cache!(File.open(file_path('test.jpg'))) 111 | @uploader.should have_permissions(0777) 112 | end 113 | 114 | describe "with ensuring multipart form deactivated" do 115 | 116 | before do 117 | CarrierWave.configure do |config| 118 | config.ensure_multipart_form = false 119 | end 120 | end 121 | 122 | it "should not raise an error when trying to cache a string" do 123 | running { 124 | @uploader.cache!(file_path('test.jpg')) 125 | }.should_not raise_error(CarrierWave::FormNotMultipart) 126 | end 127 | 128 | it "should raise an error when trying to cache a pathname and " do 129 | running { 130 | @uploader.cache!(Pathname.new(file_path('test.jpg'))) 131 | }.should_not raise_error(CarrierWave::FormNotMultipart) 132 | end 133 | 134 | end 135 | end 136 | 137 | describe '#retrieve_from_cache!' do 138 | it "should cache a file" do 139 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 140 | @uploader.file.should be_an_instance_of(CarrierWave::SanitizedFile) 141 | end 142 | 143 | it "should be cached" do 144 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 145 | @uploader.should be_cached 146 | end 147 | 148 | it "should set the path to the tmp dir" do 149 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 150 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpeg') 151 | end 152 | 153 | it "should overwrite a file that has already been cached" do 154 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 155 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/bork.txt') 156 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/bork.txt') 157 | end 158 | 159 | it "should store the cache_name" do 160 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 161 | @uploader.cache_name.should == '20071201-1234-345-2255/test.jpeg' 162 | end 163 | 164 | it "should store the filename" do 165 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 166 | @uploader.filename.should == 'test.jpeg' 167 | end 168 | 169 | it "should set the url" do 170 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpeg') 171 | @uploader.url.should == '/uploads/tmp/20071201-1234-345-2255/test.jpeg' 172 | end 173 | 174 | it "should raise an error when the cache_id has an invalid format" do 175 | running { 176 | @uploader.retrieve_from_cache!('12345/test.jpeg') 177 | }.should raise_error(CarrierWave::InvalidParameter) 178 | 179 | @uploader.file.should be_nil 180 | @uploader.filename.should be_nil 181 | @uploader.cache_name.should be_nil 182 | end 183 | 184 | it "should raise an error when the original_filename contains invalid characters" do 185 | running { 186 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/te/st.jpeg') 187 | }.should raise_error(CarrierWave::InvalidParameter) 188 | running { 189 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/te??%st.jpeg') 190 | }.should raise_error(CarrierWave::InvalidParameter) 191 | 192 | @uploader.file.should be_nil 193 | @uploader.filename.should be_nil 194 | @uploader.cache_name.should be_nil 195 | end 196 | end 197 | 198 | describe 'with an overridden, reversing, filename' do 199 | before do 200 | @uploader_class.class_eval do 201 | def filename 202 | super.reverse unless super.blank? 203 | end 204 | end 205 | end 206 | 207 | describe '#cache!' do 208 | 209 | before do 210 | CarrierWave.stub!(:generate_cache_id).and_return('20071201-1234-345-2255') 211 | end 212 | 213 | it "should set the filename to the file's reversed filename" do 214 | @uploader.cache!(File.open(file_path('test.jpg'))) 215 | @uploader.filename.should == "gpj.tset" 216 | end 217 | 218 | it "should move it to the tmp dir with the filename unreversed" do 219 | @uploader.cache!(File.open(file_path('test.jpg'))) 220 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 221 | @uploader.file.exists?.should be_true 222 | end 223 | end 224 | 225 | describe '#retrieve_from_cache!' do 226 | it "should set the path to the tmp dir" do 227 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpg') 228 | @uploader.current_path.should == public_path('uploads/tmp/20071201-1234-345-2255/test.jpg') 229 | end 230 | 231 | it "should set the filename to the reversed name of the file" do 232 | @uploader.retrieve_from_cache!('20071201-1234-345-2255/test.jpg') 233 | @uploader.filename.should == "gpj.tset" 234 | end 235 | end 236 | end 237 | 238 | end 239 | --------------------------------------------------------------------------------