├── spec ├── fixtures │ ├── case.JPG │ ├── new.jpeg │ ├── old.jpeg │ ├── test+.jpg │ ├── test.jpeg │ ├── test.jpg │ ├── Uppercase.jpg │ ├── sponsored.doc │ ├── ruby.gif │ ├── юникод.jpg │ ├── landscape.jpg │ ├── portrait.jpg │ ├── multi_page.pdf │ ├── bork.txt │ ├── new.txt │ ├── old.txt │ ├── bork.ttxt │ └── bork.txtt ├── support │ ├── file_utils_helper.rb │ └── activerecord.rb ├── uploader │ ├── paths_spec.rb │ ├── mountable_spec.rb │ ├── callback_spec.rb │ ├── file_size_spec.rb │ ├── proxy_spec.rb │ ├── default_url_spec.rb │ ├── remove_spec.rb │ ├── content_type_blacklist_spec.rb │ ├── content_type_whitelist_spec.rb │ ├── overrides_spec.rb │ ├── configuration_spec.rb │ ├── extension_blacklist_spec.rb │ ├── extension_whitelist_spec.rb │ ├── processing_spec.rb │ ├── url_spec.rb │ └── download_spec.rb ├── generators │ └── uploader_generator_spec.rb ├── storage │ ├── fog_spec.rb │ ├── fog_credentials.rb │ └── file_spec.rb ├── spec_helper.rb └── compatibility │ └── paperclip_spec.rb ├── features ├── fixtures │ ├── bork.txt │ ├── monkey.txt │ └── upcased_bork.txt ├── step_definitions │ ├── download_steps.rb │ ├── caching_steps.rb │ ├── store_steps.rb │ ├── activerecord_steps.rb │ ├── mount_steps.rb │ ├── datamapper_steps.rb │ ├── file_steps.rb │ └── general_steps.rb ├── support │ ├── env.rb │ └── activerecord.rb ├── download.feature ├── caching.feature ├── file_storage.feature ├── file_storage_overridden_filename.feature ├── file_storage_overridden_store_dir.feature ├── versions_caching_from_versions.feature ├── mount_activerecord.feature ├── file_storage_reversing_processor.feature ├── versions_overriden_store_dir.feature ├── versions_basics.feature ├── versions_overridden_filename.feature └── versions_nested_versions.feature ├── lib ├── carrierwave │ ├── version.rb │ ├── storage.rb │ ├── processing.rb │ ├── utilities.rb │ ├── error.rb │ ├── uploader │ │ ├── default_url.rb │ │ ├── remove.rb │ │ ├── serialization.rb │ │ ├── callbacks.rb │ │ ├── url.rb │ │ ├── mountable.rb │ │ ├── file_size.rb │ │ ├── content_type_whitelist.rb │ │ ├── content_type_blacklist.rb │ │ ├── extension_blacklist.rb │ │ ├── extension_whitelist.rb │ │ ├── proxy.rb │ │ ├── magic_mime_blacklist.rb │ │ ├── magic_mime_whitelist.rb │ │ ├── processing.rb │ │ ├── store.rb │ │ ├── download.rb │ │ ├── cache.rb │ │ └── configuration.rb │ ├── utilities │ │ └── uri.rb │ ├── locale │ │ └── en.yml │ ├── storage │ │ ├── abstract.rb │ │ └── file.rb │ ├── uploader.rb │ ├── validations │ │ └── active_model.rb │ ├── orm │ │ └── activerecord.rb │ ├── compatibility │ │ └── paperclip.rb │ └── mounter.rb ├── generators │ ├── uploader_generator.rb │ └── templates │ │ └── uploader.rb └── carrierwave.rb ├── cucumber.yml ├── Gemfile ├── gemfiles ├── rails-4-1.gemfile ├── rails-4-2.gemfile ├── rails-4-0.gemfile ├── rails-5-0.gemfile ├── rails-5-1.gemfile └── rails-master.gemfile ├── .gitignore ├── script ├── destroy ├── generate └── console ├── Rakefile ├── CONTRIBUTING.md ├── carrierwave.gemspec ├── .travis.yml └── .rubocop.yml /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+.jpg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/test.jpeg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/test.jpg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /features/fixtures/bork.txt: -------------------------------------------------------------------------------- 1 | this is a file -------------------------------------------------------------------------------- /spec/fixtures/Uppercase.jpg: -------------------------------------------------------------------------------- 1 | this is stuff -------------------------------------------------------------------------------- /spec/fixtures/sponsored.doc: -------------------------------------------------------------------------------- 1 | Hi there 2 | -------------------------------------------------------------------------------- /features/fixtures/monkey.txt: -------------------------------------------------------------------------------- 1 | this is another file -------------------------------------------------------------------------------- /features/fixtures/upcased_bork.txt: -------------------------------------------------------------------------------- 1 | THIS IS A FILE -------------------------------------------------------------------------------- /lib/carrierwave/version.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | VERSION = "1.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --format pretty --no-source 2 | html: --format html --out features.html -------------------------------------------------------------------------------- /spec/fixtures/ruby.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/carrierwave/master/spec/fixtures/ruby.gif -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activemodel-serializers-xml" 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /spec/fixtures/юникод.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/carrierwave/master/spec/fixtures/юникод.jpg -------------------------------------------------------------------------------- /lib/carrierwave/storage.rb: -------------------------------------------------------------------------------- 1 | require "carrierwave/storage/abstract" 2 | require "carrierwave/storage/file" 3 | -------------------------------------------------------------------------------- /spec/fixtures/landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/carrierwave/master/spec/fixtures/landscape.jpg -------------------------------------------------------------------------------- /spec/fixtures/portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/carrierwave/master/spec/fixtures/portrait.jpg -------------------------------------------------------------------------------- /spec/fixtures/multi_page.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/carrierwave/master/spec/fixtures/multi_page.pdf -------------------------------------------------------------------------------- /lib/carrierwave/processing.rb: -------------------------------------------------------------------------------- 1 | require "carrierwave/processing/rmagick" 2 | require "carrierwave/processing/mini_magick" 3 | -------------------------------------------------------------------------------- /gemfiles/rails-4-1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 4.1.0" 4 | 5 | gemspec :path => "../" 6 | -------------------------------------------------------------------------------- /gemfiles/rails-4-2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 4.2.0" 4 | 5 | gemspec :path => "../" 6 | -------------------------------------------------------------------------------- /lib/carrierwave/utilities.rb: -------------------------------------------------------------------------------- 1 | require 'carrierwave/utilities/uri' 2 | 3 | module CarrierWave 4 | module Utilities 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /gemfiles/rails-4-0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 4.0.0" 4 | gem "railties", "~> 4.0.0" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/rails-5-0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 5.0.0" 4 | gem "activemodel-serializers-xml" 5 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "rails-5", platforms: :jruby 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails-5-1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 5.1.0" 4 | gem "activemodel-serializers-xml" 5 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "rails-5", platforms: :jruby 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /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", File.join('app/uploaders', class_path, "#{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 | stub_request(:get, "s3.amazonaws.com/Monkey/testfile.txt"). 4 | to_return(body: "S3 Remote File", headers: { "Content-Type" => "text/plain" }) 5 | end 6 | 7 | @uploader.download!(url) 8 | end 9 | -------------------------------------------------------------------------------- /lib/carrierwave/error.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | class UploadError < StandardError; end 3 | class IntegrityError < UploadError; end 4 | class InvalidParameter < UploadError; end 5 | class ProcessingError < UploadError; end 6 | class DownloadError < UploadError; end 7 | class UnknownStorageError < StandardError; end 8 | end 9 | -------------------------------------------------------------------------------- /.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/fixtures/*_copy.png 12 | spec/test.log 13 | spec/tmp 14 | *.swp 15 | .rvmrc 16 | .idea 17 | .bundle 18 | Gemfile.lock 19 | gemfiles/*.lock 20 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/carrierwave/uploader/default_url.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module DefaultUrl 4 | 5 | def url(*args) 6 | super || default_url(*args) 7 | end 8 | 9 | ## 10 | # Override this method in your uploader to provide a default url 11 | # in case no file has been cached/stored yet. 12 | # 13 | def default_url(*args); end 14 | 15 | end # DefaultPath 16 | end # Uploader 17 | end # CarrierWave 18 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /features/step_definitions/caching_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^the file '(.*?)' is cached file at '(.*?)'$/ do |file, cached| 2 | FileUtils.mkdir_p(File.dirname(file_path(cached))) 3 | FileUtils.cp(file_path(file), file_path(cached)) 4 | end 5 | 6 | When /^I cache the file '(.*?)'$/ do |file| 7 | @uploader.cache!(File.open(file_path(file))) 8 | end 9 | 10 | When /^I retrieve the cache name '(.*?)' from the cache$/ do |name| 11 | @uploader.retrieve_from_cache!(name) 12 | end 13 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /spec/support/file_utils_helper.rb: -------------------------------------------------------------------------------- 1 | module FileUtilsHelper 2 | # NOTE: Make FileUtils.mkdir_p to raise `Errno::EMLINK` only once 3 | def fake_failed_mkdir_p 4 | original_mkdir_p = FileUtils.method(:mkdir_p) 5 | mkdir_p_called = false 6 | allow(FileUtils).to receive(:mkdir_p) do |args| 7 | if mkdir_p_called 8 | original_mkdir_p.call(*args) 9 | else 10 | mkdir_p_called = true 11 | raise Errno::EMLINK 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.join('..', '..', 'lib'), File.dirname(__FILE__)) 2 | 3 | require File.join(File.dirname(__FILE__), 'activerecord') 4 | 5 | require 'rspec' 6 | require 'carrierwave' 7 | require "webmock/cucumber" 8 | 9 | alias :running :lambda 10 | 11 | def file_path( *paths ) 12 | File.expand_path(File.join('..', *paths), File.dirname(__FILE__)) 13 | end 14 | 15 | CarrierWave.root = file_path('public') 16 | 17 | After do 18 | FileUtils.rm_rf(file_path("public")) 19 | end 20 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/remove.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Remove 4 | extend ActiveSupport::Concern 5 | 6 | include CarrierWave::Uploader::Callbacks 7 | 8 | ## 9 | # Removes the file and reset it 10 | # 11 | def remove! 12 | with_callbacks(:remove) do 13 | @file.delete if @file 14 | @file = nil 15 | @cache_id = nil 16 | end 17 | end 18 | 19 | end # Remove 20 | end # Uploader 21 | end # CarrierWave 22 | -------------------------------------------------------------------------------- /features/support/activerecord.rb: -------------------------------------------------------------------------------- 1 | require 'carrierwave/mount' 2 | require File.join(File.dirname(__FILE__), '..', '..', 'spec', 'support', 'activerecord') 3 | 4 | class TestMigration < ActiveRecord.version.to_s >= '5.0' ? ActiveRecord::Migration[5.0] : ActiveRecord::Migration 5 | def self.up 6 | create_table :users, :force => true do |t| 7 | t.column :avatar, :string 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :users 13 | end 14 | end 15 | 16 | Before do 17 | TestMigration.up 18 | end 19 | -------------------------------------------------------------------------------- /features/step_definitions/store_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^the file '(.*?)' is stored at '(.*?)'$/ do |file, stored| 2 | FileUtils.mkdir_p(File.dirname(file_path(stored))) 3 | FileUtils.cp(file_path(file), file_path(stored)) 4 | end 5 | 6 | When /^I store the file$/ do 7 | @uploader.store! 8 | end 9 | 10 | When /^I store the file '(.*?)'$/ do |file| 11 | @uploader.store!(File.open(file_path(file))) 12 | end 13 | 14 | When /^I retrieve the file '(.*?)' from the store$/ do |identifier| 15 | @uploader.retrieve_from_store!(identifier) 16 | end 17 | -------------------------------------------------------------------------------- /spec/uploader/paths_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | 7 | after { FileUtils.rm_rf(public_path) } 8 | 9 | describe '#root' do 10 | describe "default behavior" do 11 | before { CarrierWave.root = public_path } 12 | 13 | it "defaults to the current value of CarrierWave.root" do 14 | expect(uploader.root).to eq(public_path) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /features/step_definitions/activerecord_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^an activerecord class that uses the '([^\']*)' table$/ do |name| 2 | @mountee_klass = Class.new(ActiveRecord::Base) 3 | @mountee_klass.table_name = name 4 | end 5 | 6 | Given /^an instance of the activerecord class$/ do 7 | @instance = @mountee_klass.new 8 | end 9 | 10 | When /^I save the active record$/ do 11 | @instance.save! 12 | end 13 | 14 | When /^I reload the active record$/ do 15 | @instance = @instance.class.find(@instance.id) 16 | end 17 | 18 | When /^I delete the active record$/ do 19 | @instance.destroy 20 | end 21 | -------------------------------------------------------------------------------- /lib/carrierwave/utilities/uri.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Utilities 3 | module Uri 4 | # based on Ruby < 2.0's URI.encode 5 | SAFE_STRING = URI::REGEXP::PATTERN::UNRESERVED + '\/' 6 | UNSAFE = Regexp.new("[^#{SAFE_STRING}]", false) 7 | 8 | private 9 | def encode_path(path) 10 | path.to_s.gsub(UNSAFE) do 11 | us = $& 12 | tmp = '' 13 | us.each_byte do |uc| 14 | tmp << sprintf('%%%02X', uc) 15 | end 16 | tmp 17 | end 18 | end 19 | end # Uri 20 | end # Utilities 21 | end # CarrierWave 22 | -------------------------------------------------------------------------------- /gemfiles/rails-master.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", github: "rails/rails", branch: "master" 4 | gem "rack", github: "rack/rack", branch: "master" 5 | gem "arel", github: "rails/arel", branch: "master" 6 | gem "sprockets", github: "rails/sprockets", branch: "master" 7 | gem "sprockets-rails", github: "rails/sprockets-rails", branch: "master" 8 | gem "sass-rails", github: "rails/sass-rails" 9 | gem "activemodel-serializers-xml" 10 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "rails-5", platforms: :jruby 11 | 12 | gemspec :path => "../" 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | require 'bundler' 9 | Bundler::GemHelper.install_tasks 10 | 11 | require 'rake' 12 | require 'rspec/core/rake_task' 13 | require 'cucumber' 14 | require 'cucumber/rake/task' 15 | 16 | desc "Run all examples" 17 | RSpec::Core::RakeTask.new(:spec) do |t| 18 | t.rspec_opts = %w[--color] 19 | end 20 | 21 | desc "Run cucumber features" 22 | Cucumber::Rake::Task.new(:features) do |t| 23 | t.cucumber_opts = "features --format progress" 24 | end 25 | 26 | task :default => [:spec, :features] 27 | -------------------------------------------------------------------------------- /spec/generators/uploader_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'generator_spec' 3 | require 'generators/uploader_generator' 4 | 5 | describe UploaderGenerator, :type => :generator do 6 | destination(File.expand_path("../../tmp", __FILE__)) 7 | 8 | before { prepare_destination } 9 | 10 | it "creates uploader file" do 11 | run_generator %w(Avatar) 12 | assert_file 'app/uploaders/avatar_uploader.rb', /class AvatarUploader < CarrierWave::Uploader::Base/ 13 | end 14 | 15 | it "creates namespaced uploader file" do 16 | run_generator %w(MyModule::Avatar) 17 | assert_file 'app/uploaders/my_module/avatar_uploader.rb', /class MyModule::AvatarUploader < CarrierWave::Uploader::Base/ 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /features/step_definitions/mount_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I assign the file '([^\']*)' to the '([^\']*)' column$/ do |path, column| 2 | @instance.send("#{column}=", File.open(file_path(path))) 3 | end 4 | 5 | Given /^the uploader class is mounted on the '([^\']*)' column$/ do |column| 6 | @mountee_klass.mount_uploader column.to_sym, @klass 7 | end 8 | 9 | When /^I retrieve the file later from the cache name for the column '([^\']*)'$/ do |column| 10 | new_instance = @instance.class.new 11 | new_instance.send("#{column}_cache=", @instance.send("#{column}_cache")) 12 | @instance = new_instance 13 | end 14 | 15 | Then /^the url for the column '([^\']*)' should be '([^\']*)'$/ do |column, url| 16 | @instance.send("#{column}_url").should == url 17 | end 18 | -------------------------------------------------------------------------------- /features/step_definitions/datamapper_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^a datamapper class that has a '([^\']*)' column$/ do |column| 2 | @mountee_klass = Class.new do 3 | include DataMapper::Resource 4 | 5 | storage_names[:default] = 'users' 6 | 7 | property :id, DataMapper::Types::Serial 8 | property column.to_sym, String 9 | end 10 | @mountee_klass.auto_migrate! 11 | end 12 | 13 | Given /^an instance of the datamapper class$/ do 14 | @instance = @mountee_klass.new 15 | end 16 | 17 | When /^I save the datamapper record$/ do 18 | @instance.save 19 | end 20 | 21 | When /^I reload the datamapper record$/ do 22 | @instance = @instance.class.first(:id => @instance.key) 23 | end 24 | 25 | When /^I delete the datamapper record$/ do 26 | @instance.destroy 27 | end 28 | -------------------------------------------------------------------------------- /spec/uploader/mountable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | 7 | after { FileUtils.rm_rf(public_path) } 8 | 9 | describe '#model' do 10 | let(:model) { double('a model object') } 11 | let(:uploader) { uploader_class.new(model) } 12 | 13 | it "is remembered from initialization" do 14 | expect(uploader.model).to eq(model) 15 | end 16 | end 17 | 18 | describe '#mounted_as' do 19 | let(:model) { double('a model object') } 20 | let(:uploader) { uploader_class.new(model, :llama) } 21 | 22 | it "is remembered from initialization" do 23 | expect(uploader.mounted_as).to eq(:llama) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/serialization.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "active_support/core_ext/hash" 3 | 4 | module CarrierWave 5 | module Uploader 6 | module Serialization 7 | extend ActiveSupport::Concern 8 | 9 | def serializable_hash(options = nil) 10 | {"url" => url}.merge Hash[versions.map { |name, version| [name, { "url" => version.url }] }] 11 | end 12 | 13 | def as_json(options=nil) 14 | serializable_hash 15 | end 16 | 17 | def to_json(options=nil) 18 | JSON.generate(as_json) 19 | end 20 | 21 | def to_xml(options={}) 22 | merged_options = options.merge(:root => mounted_as || "uploader", :type => 'uploader') 23 | serializable_hash.to_xml(merged_options) 24 | end 25 | 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/support/activerecord.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'jruby' 2 | require 'activerecord-jdbcpostgresql-adapter' 3 | else 4 | require 'pg' 5 | end 6 | require 'active_record' 7 | require 'carrierwave/orm/activerecord' 8 | Bundler.require 9 | 10 | # Change this if PG is unavailable 11 | dbconfig = { 12 | :adapter => 'postgresql', 13 | :database => 'carrierwave_test', 14 | :encoding => 'utf8', 15 | :username => 'postgres' 16 | } 17 | 18 | database = dbconfig.delete(:database) 19 | 20 | ActiveRecord::Base.establish_connection(dbconfig.merge(database: "template1")) 21 | begin 22 | ActiveRecord::Base.connection.create_database database 23 | rescue ActiveRecord::StatementInvalid => e # database already exists 24 | end 25 | ActiveRecord::Base.establish_connection(dbconfig.merge(:database => database)) 26 | 27 | ActiveRecord::Migration.verbose = false 28 | 29 | if ActiveRecord::VERSION::STRING >= '4.2' && ActiveRecord::VERSION::STRING < '5.0' 30 | ActiveRecord::Base.raise_in_transactional_callbacks = true 31 | end 32 | -------------------------------------------------------------------------------- /lib/carrierwave/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | carrierwave_processing_error: failed to be processed 5 | carrierwave_integrity_error: is not of an allowed file type 6 | carrierwave_download_error: could not be downloaded 7 | extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}" 8 | extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}" 9 | content_type_whitelist_error: "You are not allowed to upload %{content_type} files" 10 | content_type_blacklist_error: "You are not allowed to upload %{content_type} files" 11 | rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?" 12 | mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}" 13 | min_size_error: "File size should be greater than %{min_size}" 14 | max_size_error: "File size should be less than %{max_size}" 15 | -------------------------------------------------------------------------------- /spec/uploader/callback_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | describe "callback isolation" do 5 | let(:default_before_callbacks) do 6 | [ 7 | :check_extension_whitelist!, 8 | :check_extension_blacklist!, 9 | :check_content_type_whitelist!, 10 | :check_content_type_blacklist!, 11 | :check_size!, 12 | :process! 13 | ] 14 | end 15 | 16 | let(:uploader_class_1) { Class.new(CarrierWave::Uploader::Base) } 17 | let(:uploader_class_2) { Class.new(CarrierWave::Uploader::Base) } 18 | 19 | before { uploader_class_2.before(:cache, :before_cache_callback) } 20 | 21 | it { expect(uploader_class_1._before_callbacks[:cache]).to eq(default_before_callbacks) } 22 | 23 | 24 | it { expect(uploader_class_2._before_callbacks[:cache]).to eq(default_before_callbacks + [:before_cache_callback]) } 25 | 26 | it "doesn't inherit the uploader 2 callback" do 27 | expect(uploader_class_1._before_callbacks[:cache]).to eq(default_before_callbacks) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/callbacks.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Callbacks 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | class_attribute :_before_callbacks, :_after_callbacks, 8 | :instance_writer => false 9 | self._before_callbacks = Hash.new [] 10 | self._after_callbacks = Hash.new [] 11 | end 12 | 13 | def with_callbacks(kind, *args) 14 | self.class._before_callbacks[kind].each { |c| send c, *args } 15 | yield 16 | self.class._after_callbacks[kind].each { |c| send c, *args } 17 | end 18 | 19 | module ClassMethods 20 | def before(kind, callback) 21 | self._before_callbacks = self._before_callbacks. 22 | merge kind => _before_callbacks[kind] + [callback] 23 | end 24 | 25 | def after(kind, callback) 26 | self._after_callbacks = self._after_callbacks. 27 | merge kind => _after_callbacks[kind] + [callback] 28 | end 29 | end # ClassMethods 30 | 31 | end # Callbacks 32 | end # Uploader 33 | end # CarrierWave 34 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/url.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Url 4 | extend ActiveSupport::Concern 5 | include CarrierWave::Uploader::Configuration 6 | include CarrierWave::Utilities::Uri 7 | 8 | ## 9 | # === Parameters 10 | # 11 | # [Hash] optional, the query params (only AWS) 12 | # 13 | # === Returns 14 | # 15 | # [String] the location where this file is accessible via a url 16 | # 17 | def url(options = {}) 18 | if file.respond_to?(:url) and not (tmp_url = file.url).blank? 19 | file.method(:url).arity == 0 ? tmp_url : file.url(options) 20 | elsif file.respond_to?(:path) 21 | path = encode_path(file.path.sub(File.expand_path(root), '')) 22 | 23 | if host = asset_host 24 | if host.respond_to? :call 25 | "#{host.call(file)}#{path}" 26 | else 27 | "#{host}#{path}" 28 | end 29 | else 30 | (base_path || "") + path 31 | end 32 | end 33 | end 34 | 35 | def to_s 36 | url || '' 37 | end 38 | 39 | end # Url 40 | end # Uploader 41 | end # CarrierWave 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CarrierWave 2 | 3 | CarrierWave thrives on a large number of [contributors](https://github.com/carrierwaveuploader/carrierwave/contributors), 4 | and pull requests are very welcome. Before submitting a pull request, please make sure that your changes are well tested. 5 | 6 | First, make sure you have `imagemagick` and `ghostscript` installed. You may need `libmagic` as well. 7 | 8 | Then, you'll need to install bundler and the gem dependencies: 9 | 10 | `gem install bundler && bundle install` 11 | 12 | You should now be able to run the local tests: 13 | 14 | `bundle exec rake` 15 | 16 | You can also run the remote specs by creating a ~/.fog file: 17 | 18 | ```yaml 19 | :carrierwave: 20 | :aws_access_key_id: xxx 21 | :aws_secret_access_key: yyy 22 | :rackspace_username: xxx 23 | :rackspace_api_key: yyy 24 | :google_storage_access_key_id: xxx 25 | :google_storage_secret_access_key: yyy 26 | ``` 27 | 28 | You should now be able to run the remote tests: 29 | 30 | REMOTE=true bundle exec rake 31 | 32 | Please test with the latest Ruby 2.2.x version using RVM if possible. 33 | 34 | ## Running active record tests 35 | 36 | Make sure you have a local PostgreSQL database named `carrierwave_test` with the username 37 | `postgres` 38 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/mountable.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Mountable 4 | 5 | attr_reader :model, :mounted_as 6 | 7 | ## 8 | # If a model is given as the first parameter, it will be stored in the 9 | # uploader, and available through +#model+. Likewise, mounted_as stores 10 | # the name of the column where this instance of the uploader is mounted. 11 | # These values can then be used inside your uploader. 12 | # 13 | # If you do not wish to mount your uploaders with the ORM extensions in 14 | # -more then you can override this method inside your uploader. Just be 15 | # sure to call +super+ 16 | # 17 | # === Parameters 18 | # 19 | # [model (Object)] Any kind of model object 20 | # [mounted_as (Symbol)] The name of the column where this uploader is mounted 21 | # 22 | # === Examples 23 | # 24 | # class MyUploader < CarrierWave::Uploader::Base 25 | # 26 | # def store_dir 27 | # File.join('public', 'files', mounted_as, model.permalink) 28 | # end 29 | # end 30 | # 31 | def initialize(model=nil, mounted_as=nil) 32 | @model = model 33 | @mounted_as = mounted_as 34 | end 35 | 36 | end # Mountable 37 | end # Uploader 38 | end # CarrierWave 39 | -------------------------------------------------------------------------------- /spec/storage/fog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fog' 3 | require 'carrierwave/storage/fog' 4 | 5 | unless ENV['REMOTE'] == 'true' 6 | Fog.mock! 7 | end 8 | 9 | require_relative './fog_credentials' # after Fog.mock! 10 | require_relative './fog_helper' 11 | 12 | FOG_CREDENTIALS.each do |credential| 13 | fog_tests(credential) 14 | end 15 | 16 | describe CarrierWave::Storage::Fog::File do 17 | subject(:file) { CarrierWave::Storage::Fog::File.new(nil, nil, nil) } 18 | 19 | describe "#filename" do 20 | subject(:filename) { file.filename } 21 | 22 | before { allow(file).to receive(:url).and_return(url) } 23 | 24 | context "with normal url" do 25 | let(:url) { 'http://example.com/path/to/foo.txt' } 26 | 27 | it "extracts filename from url" do 28 | is_expected.to eq('foo.txt') 29 | end 30 | end 31 | 32 | context "when url contains '/' in query string" do 33 | let(:url){ 'http://example.com/path/to/foo.txt?bar=baz/fubar' } 34 | 35 | it "extracts correct part" do 36 | is_expected.to eq('foo.txt') 37 | end 38 | end 39 | 40 | context "when url contains multi-byte characters" do 41 | let(:url) { 'http://example.com/path/to/%E6%97%A5%E6%9C%AC%E8%AA%9E.txt' } 42 | 43 | it "decodes multi-byte characters" do 44 | is_expected.to eq('日本語.txt') 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/abstract.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Storage 3 | 4 | ## 5 | # This file serves mostly as a specification for Storage engines. There is no requirement 6 | # that storage engines must be a subclass of this class. 7 | # 8 | class Abstract 9 | 10 | attr_reader :uploader 11 | 12 | def initialize(uploader) 13 | @uploader = uploader 14 | end 15 | 16 | def identifier 17 | uploader.filename 18 | end 19 | 20 | def store!(file) 21 | end 22 | 23 | def retrieve!(identifier) 24 | end 25 | 26 | def cache!(new_file) 27 | raise NotImplementedError.new("Need to implement #cache! if you want to use #{self.class.name} as a cache storage.") 28 | end 29 | 30 | def retrieve_from_cache!(identifier) 31 | raise NotImplementedError.new("Need to implement #retrieve_from_cache! if you want to use #{self.class.name} as a cache storage.") 32 | end 33 | 34 | def delete_dir!(path) 35 | raise NotImplementedError.new("Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage.") 36 | end 37 | 38 | def clean_cache!(seconds) 39 | raise NotImplementedError.new("Need to implement #clean_cache! if you want to use #{self.class.name} as a cache storage.") 40 | end 41 | end # Abstract 42 | end # Storage 43 | end # CarrierWave 44 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/file_size.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | 3 | module CarrierWave 4 | module Uploader 5 | module FileSize 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | before :cache, :check_size! 10 | end 11 | 12 | ## 13 | # Override this method in your uploader to provide a Range of Size which 14 | # are allowed to be uploaded. 15 | # === Returns 16 | # 17 | # [NilClass, Range] a size range which are permitted to be uploaded 18 | # 19 | # === Examples 20 | # 21 | # def size_range 22 | # 3256...5748 23 | # end 24 | # 25 | def size_range; end 26 | 27 | private 28 | 29 | def check_size!(new_file) 30 | size = new_file.size 31 | expected_size_range = size_range 32 | if expected_size_range.is_a?(::Range) 33 | if size < expected_size_range.min 34 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.min_size_error", :min_size => ActiveSupport::NumberHelper.number_to_human_size(expected_size_range.min)) 35 | elsif size > expected_size_range.max 36 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.max_size_error", :max_size => ActiveSupport::NumberHelper.number_to_human_size(expected_size_range.max)) 37 | end 38 | end 39 | end 40 | 41 | end # FileSize 42 | end # Uploader 43 | end # CarrierWave 44 | -------------------------------------------------------------------------------- /spec/storage/fog_credentials.rb: -------------------------------------------------------------------------------- 1 | unless defined?(FOG_CREDENTIALS) 2 | 3 | credentials = [] 4 | 5 | if Fog.mocking? 6 | # Local and Rackspace don't have fog double 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 | mappings.each do |provider, keys| 15 | data = {:provider => provider} 16 | keys.each do |key| 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 | mappings.each do |provider, keys| 34 | unless (creds = Fog.credentials.reject {|key, value| ![*keys].include?(key)}).empty? 35 | data = {:provider => provider} 36 | keys.each do |key| 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 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/content_type_whitelist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module ContentTypeWhitelist 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before :cache, :check_content_type_whitelist! 8 | end 9 | 10 | ## 11 | # Override this method in your uploader to provide a whitelist of files content types 12 | # which are allowed to be uploaded. 13 | # Not only strings but Regexp are allowed as well. 14 | # 15 | # === Returns 16 | # 17 | # [NilClass, String, Regexp, Array[String, Regexp]] a whitelist of content types which are allowed to be uploaded 18 | # 19 | # === Examples 20 | # 21 | # def content_type_whitelist 22 | # %w(text/json application/json) 23 | # end 24 | # 25 | # Basically the same, but using a Regexp: 26 | # 27 | # def content_type_whitelist 28 | # [/(text|application)\/json/] 29 | # end 30 | # 31 | def content_type_whitelist; end 32 | 33 | private 34 | 35 | def check_content_type_whitelist!(new_file) 36 | content_type = new_file.content_type 37 | if content_type_whitelist && !whitelisted_content_type?(content_type) 38 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.content_type_whitelist_error", content_type: content_type) 39 | end 40 | end 41 | 42 | def whitelisted_content_type?(content_type) 43 | Array(content_type_whitelist).any? { |item| content_type =~ /#{item}/ } 44 | end 45 | 46 | end # ContentTypeWhitelist 47 | end # Uploader 48 | end # CarrierWave 49 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/content_type_blacklist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module ContentTypeBlacklist 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before :cache, :check_content_type_blacklist! 8 | end 9 | 10 | ## 11 | # Override this method in your uploader to provide a blacklist of files content types 12 | # which are not allowed to be uploaded. 13 | # Not only strings but Regexp are allowed as well. 14 | # 15 | # === Returns 16 | # 17 | # [NilClass, String, Regexp, Array[String, Regexp]] a blacklist of content types which are not allowed to be uploaded 18 | # 19 | # === Examples 20 | # 21 | # def content_type_blacklist 22 | # %w(text/json application/json) 23 | # end 24 | # 25 | # Basically the same, but using a Regexp: 26 | # 27 | # def content_type_blacklist 28 | # [/(text|application)\/json/] 29 | # end 30 | # 31 | def content_type_blacklist; end 32 | 33 | private 34 | 35 | def check_content_type_blacklist!(new_file) 36 | content_type = new_file.content_type 37 | if content_type_blacklist && blacklisted_content_type?(content_type) 38 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.content_type_blacklist_error", content_type: content_type) 39 | end 40 | end 41 | 42 | def blacklisted_content_type?(content_type) 43 | Array(content_type_blacklist).any? { |item| content_type =~ /#{item}/ } 44 | end 45 | 46 | end # ContentTypeBlacklist 47 | end # Uploader 48 | end # CarrierWave 49 | -------------------------------------------------------------------------------- /spec/uploader/file_size_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | let(:cache_id) { '20071201-1234-1234-2255' } 7 | let(:test_file) { File.open(file_path('test.jpg')) } 8 | 9 | after { FileUtils.rm_rf(public_path) } 10 | 11 | describe '#cache!' do 12 | subject { lambda { uploader.cache!(test_file) } } 13 | 14 | before { allow(CarrierWave).to receive(:generate_cache_id).and_return(cache_id) } 15 | 16 | describe "file size range" do 17 | before { allow(uploader).to receive(:size_range).and_return(range) } 18 | 19 | context "when not specified" do 20 | let(:range) { nil } 21 | 22 | it "doesn't raise an integrity error" do 23 | is_expected.not_to raise_error 24 | end 25 | end 26 | 27 | context "when below the minimum" do 28 | let(:range) { 2097152..4194304 } 29 | 30 | it "raises an integrity error" do 31 | is_expected.to raise_error(CarrierWave::IntegrityError, 'File size should be greater than 2 MB') 32 | end 33 | end 34 | 35 | context "when above the maximum" do 36 | let(:range) { 0..10 } 37 | 38 | it "raises an integrity error" do 39 | is_expected.to raise_error(CarrierWave::IntegrityError, 'File size should be less than 10 Bytes') 40 | end 41 | end 42 | 43 | context "when inside the range" do 44 | let(:range) { 0..100 } 45 | 46 | it "doesn't raise an integrity error" do 47 | is_expected.not_to raise_error 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/generators/templates/uploader.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Uploader < CarrierWave::Uploader::Base 2 | 3 | # Include RMagick or MiniMagick support: 4 | # include CarrierWave::RMagick 5 | # include CarrierWave::MiniMagick 6 | 7 | # Choose what kind of storage to use for this uploader: 8 | storage :file 9 | # storage :fog 10 | 11 | # Override the directory where uploaded files will be stored. 12 | # This is a sensible default for uploaders that are meant to be mounted: 13 | def store_dir 14 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 15 | end 16 | 17 | # Provide a default URL as a default if there hasn't been a file uploaded: 18 | # def default_url(*args) 19 | # # For Rails 3.1+ asset pipeline compatibility: 20 | # # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_')) 21 | # 22 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_') 23 | # end 24 | 25 | # Process files as they are uploaded: 26 | # process scale: [200, 300] 27 | # 28 | # def scale(width, height) 29 | # # do something 30 | # end 31 | 32 | # Create different versions of your uploaded files: 33 | # version :thumb do 34 | # process resize_to_fit: [50, 50] 35 | # end 36 | 37 | # Add a white list of extensions which are allowed to be uploaded. 38 | # For images you might use something like this: 39 | # def extension_whitelist 40 | # %w(jpg jpeg gif png) 41 | # end 42 | 43 | # Override the filename of the uploaded files: 44 | # Avoid using model.id or version_name here, see uploader/store.rb for details. 45 | # def filename 46 | # "something.jpg" if original_filename 47 | # end 48 | 49 | end 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/1369894322-345-1234-2255/bork.txt' 27 | When I retrieve the cache name '1369894322-345-1234-2255/bork.txt' from the cache 28 | Then the uploader should have 'public/uploads/tmp/1369894322-345-1234-2255/bork.txt' as its current path 29 | -------------------------------------------------------------------------------- /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.description = "Upload files in your Ruby applications, map them to a range of ORMs, store them on different backends." 13 | s.summary = "Ruby file upload library" 14 | s.email = ["jonas.nicklas@gmail.com"] 15 | s.extra_rdoc_files = ["README.md"] 16 | s.files = Dir["{bin,lib}/**/*", "README.md"] 17 | s.homepage = %q{https://github.com/carrierwaveuploader/carrierwave} 18 | s.rdoc_options = ["--main"] 19 | s.require_paths = ["lib"] 20 | s.licenses = ["MIT"] 21 | 22 | s.required_ruby_version = ">= 2.0.0" 23 | 24 | s.add_dependency "activesupport", ">= 4.0.0" 25 | s.add_dependency "activemodel", ">= 4.0.0" 26 | s.add_dependency "mime-types", ">= 1.16" 27 | if RUBY_ENGINE == 'jruby' 28 | s.add_development_dependency 'activerecord-jdbcpostgresql-adapter' 29 | else 30 | s.add_development_dependency "pg" 31 | end 32 | s.add_development_dependency "rails", ">= 4.0.0" 33 | s.add_development_dependency "cucumber", "~> 2.3.2" 34 | s.add_development_dependency "rspec", "~> 3.4.0" 35 | s.add_development_dependency "webmock" 36 | s.add_development_dependency "fog", ">= 1.28.0" 37 | s.add_development_dependency "mini_magick", ">= 3.6.0" 38 | if RUBY_ENGINE != 'jruby' 39 | s.add_development_dependency "rmagick" 40 | end 41 | s.add_development_dependency "nokogiri", "~> 1.6.3" 42 | s.add_development_dependency "timecop", "0.7.1" 43 | s.add_development_dependency "generator_spec", ">= 0.9.1" 44 | s.add_development_dependency "pry" 45 | end 46 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/extension_blacklist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module ExtensionBlacklist 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before :cache, :check_extension_blacklist! 8 | end 9 | 10 | ## 11 | # Override this method in your uploader to provide a black list of extensions which 12 | # are prohibited to be uploaded. Compares the file's extension case insensitive. 13 | # Furthermore, not only strings but Regexp are allowed as well. 14 | # 15 | # When using a Regexp in the black list, `\A` and `\z` are automatically added to 16 | # the Regexp expression, also case insensitive. 17 | # 18 | # === Returns 19 | 20 | # [NilClass, String, Regexp, Array[String, Regexp]] a black list of extensions which are prohibited to be uploaded 21 | # 22 | # === Examples 23 | # 24 | # def extension_blacklist 25 | # %w(swf tiff) 26 | # end 27 | # 28 | # Basically the same, but using a Regexp: 29 | # 30 | # def extension_blacklist 31 | # [/swf/, 'tiff'] 32 | # end 33 | # 34 | 35 | def extension_blacklist; end 36 | 37 | private 38 | 39 | def check_extension_blacklist!(new_file) 40 | extension = new_file.extension.to_s 41 | if extension_blacklist && blacklisted_extension?(extension) 42 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_blacklist_error", extension: new_file.extension.inspect, prohibited_types: Array(extension_blacklist).join(", ")) 43 | end 44 | end 45 | 46 | def blacklisted_extension?(extension) 47 | Array(extension_blacklist).any? { |item| extension =~ /\A#{item}\z/i } 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/extension_whitelist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module ExtensionWhitelist 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before :cache, :check_extension_whitelist! 8 | end 9 | 10 | ## 11 | # Override this method in your uploader to provide a white list of extensions which 12 | # are allowed to be uploaded. Compares the file's extension case insensitive. 13 | # Furthermore, not only strings but Regexp are allowed as well. 14 | # 15 | # When using a Regexp in the white list, `\A` and `\z` are automatically added to 16 | # the Regexp expression, also case insensitive. 17 | # 18 | # === Returns 19 | # 20 | # [NilClass, String, Regexp, Array[String, Regexp]] a white list of extensions which are allowed to be uploaded 21 | # 22 | # === Examples 23 | # 24 | # def extension_whitelist 25 | # %w(jpg jpeg gif png) 26 | # end 27 | # 28 | # Basically the same, but using a Regexp: 29 | # 30 | # def extension_whitelist 31 | # [/jpe?g/, 'gif', 'png'] 32 | # end 33 | # 34 | def extension_whitelist; end 35 | 36 | private 37 | 38 | def check_extension_whitelist!(new_file) 39 | extension = new_file.extension.to_s 40 | if extension_whitelist && !whitelisted_extension?(extension) 41 | raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_whitelist_error", extension: new_file.extension.inspect, allowed_types: Array(extension_whitelist).join(", ")) 42 | end 43 | end 44 | 45 | def whitelisted_extension?(extension) 46 | downcase_extension = extension.downcase 47 | Array(extension_whitelist).any? { |item| downcase_extension =~ /\A#{item}\z/i } 48 | end 49 | 50 | end # ExtensionWhitelist 51 | end # Uploader 52 | end # CarrierWave 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | cache: bundler 4 | 5 | rvm: 6 | - 2.2.7 7 | - 2.3.4 8 | - 2.4.1 9 | - jruby-9.1.5.0 10 | 11 | gemfile: 12 | - gemfiles/rails-4-0.gemfile 13 | - gemfiles/rails-4-1.gemfile 14 | - gemfiles/rails-4-2.gemfile 15 | - gemfiles/rails-5-0.gemfile 16 | - gemfiles/rails-5-1.gemfile 17 | - gemfiles/rails-master.gemfile 18 | 19 | sudo: false 20 | 21 | before_install: 22 | - gem update bundler 23 | 24 | before_script: 25 | - psql -c 'create database carrierwave_test;' -U postgres 26 | 27 | matrix: 28 | include: 29 | - rvm: 2.0 30 | gemfile: gemfiles/rails-4-0.gemfile 31 | - rvm: 2.0 32 | gemfile: gemfiles/rails-4-1.gemfile 33 | - rvm: 2.0 34 | gemfile: gemfiles/rails-4-2.gemfile 35 | - rvm: 2.1 36 | gemfile: gemfiles/rails-4-0.gemfile 37 | - rvm: 2.1 38 | gemfile: gemfiles/rails-4-1.gemfile 39 | - rvm: 2.1 40 | gemfile: gemfiles/rails-4-2.gemfile 41 | - rvm: ruby-head 42 | gemfile: gemfiles/rails-5-1.gemfile 43 | - rvm: ruby-head 44 | gemfile: gemfiles/rails-master.gemfile 45 | - rvm: jruby-head 46 | gemfile: gemfiles/rails-5-1.gemfile 47 | - rvm: jruby-head 48 | gemfile: gemfiles/rails-master.gemfile 49 | exclude: 50 | - rvm: 2.4.1 51 | gemfile: gemfiles/rails-4-0.gemfile 52 | - rvm: 2.4.1 53 | gemfile: gemfiles/rails-4-1.gemfile 54 | - rvm: 2.4.1 55 | gemfile: gemfiles/rails-4-2.gemfile 56 | allow_failures: 57 | - rvm: ruby-head 58 | - rvm: jruby-head 59 | - gemfile: gemfiles/rails-master.gemfile 60 | - rvm: jruby-9.1.5.0 61 | gemfile: gemfiles/rails-5-1.gemfile 62 | fast_finish: true 63 | 64 | notifications: 65 | email: false 66 | slack: 67 | secure: Npzanyv/LXLIRlrNs8iTUbZNRhXlP+K2ZpjZoS2UKkr09jYyP1qdf5a//R3Lu7Yat7g2b4qTJGbaZBEMUQSVaJ6UX6quiBJjVWxjxjQ4Ugk8k/yOIAcGEGYPfS6YzRXemRwo9j4uy76cmwlv8cwEuYTSTBRK4XrdYHslX6pKSXM= 68 | 69 | addons: 70 | postgresql: "9.3" 71 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/proxy.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Proxy 4 | 5 | ## 6 | # === Returns 7 | # 8 | # [Boolean] Whether the uploaded file is blank 9 | # 10 | def blank? 11 | file.blank? 12 | end 13 | 14 | ## 15 | # === Returns 16 | # 17 | # [String] the path where the file is currently located. 18 | # 19 | def current_path 20 | file.try(:path) 21 | end 22 | 23 | alias_method :path, :current_path 24 | 25 | ## 26 | # Returns a string that uniquely identifies the last stored file 27 | # 28 | # === Returns 29 | # 30 | # [String] uniquely identifies a file 31 | # 32 | def identifier 33 | storage.try(:identifier) 34 | end 35 | 36 | ## 37 | # Read the contents of the file 38 | # 39 | # === Returns 40 | # 41 | # [String] contents of the file 42 | # 43 | def read 44 | file.try(:read) 45 | end 46 | 47 | ## 48 | # Fetches the size of the currently stored/cached file 49 | # 50 | # === Returns 51 | # 52 | # [Integer] size of the file 53 | # 54 | def size 55 | file.try(:size) || 0 56 | end 57 | 58 | ## 59 | # Return the size of the file when asked for its length 60 | # 61 | # === Returns 62 | # 63 | # [Integer] size of the file 64 | # 65 | # === Note 66 | # 67 | # This was added because of the way Rails handles length/size validations in 3.0.6 and above. 68 | # 69 | def length 70 | size 71 | end 72 | 73 | ## 74 | # Read the content type of the file 75 | # 76 | # === Returns 77 | # 78 | # [String] content type of the file 79 | # 80 | def content_type 81 | file.try(:content_type) 82 | end 83 | 84 | end # Proxy 85 | end # Uploader 86 | end # CarrierWave 87 | -------------------------------------------------------------------------------- /features/step_definitions/file_steps.rb: -------------------------------------------------------------------------------- 1 | ### 2 | # EXISTENCE 3 | 4 | Then /^there should be a file at '(.*?)'$/ do |file| 5 | File.exist?(file_path(file)).should be_truthy 6 | end 7 | 8 | Then /^there should not be a file at '(.*?)'$/ do |file| 9 | File.exist?(file_path(file)).should be_falsey 10 | end 11 | 12 | Then /^there should be a file called '(.*?)' somewhere in a subdirectory of '(.*?)'$/ do |file, directory| 13 | Dir.glob(File.join(file_path(directory), '**', file)).any?.should be_truthy 14 | end 15 | 16 | ### 17 | # IDENTICAL 18 | 19 | Then /^the file at '(.*?)' should be identical to the file at '(.*?)'$/ do |one, two| 20 | File.read(file_path(one)).should == File.read(file_path(two)) 21 | end 22 | 23 | Then /^the file at '(.*?)' should not be identical to the file at '(.*?)'$/ do |one, two| 24 | File.read(file_path(one)).should_not == File.read(file_path(two)) 25 | end 26 | 27 | Then /^the file called '(.*?)' in a subdirectory of '(.*?)' should be identical to the file at '(.*?)'$/ do |file, directory, other| 28 | File.read(Dir.glob(File.join(file_path(directory), '**', file)).first).should == File.read(file_path(other)) 29 | end 30 | 31 | Then /^the file called '(.*?)' in a subdirectory of '(.*?)' should not be identical to the file at '(.*?)'$/ do |file, directory, other| 32 | File.read(Dir.glob(File.join(file_path(directory), '**', file)).first).should_not == File.read(file_path(other)) 33 | end 34 | 35 | ### 36 | # CONTENT 37 | 38 | Then /^the file called '([^']+)' in a subdirectory of '([^']+)' should contain '([^']+)'$/ do |file, directory, content| 39 | File.read(Dir.glob(File.join(file_path(directory), '**', file)).first).should include(content) 40 | end 41 | 42 | Then /^the file at '([^']+)' should contain '([^']+)'$/ do |path, content| 43 | File.read(file_path(path)).should include(content) 44 | end 45 | 46 | ### 47 | # REVERSING 48 | 49 | Then /^the file at '(.*?)' should be the reverse of the file at '(.*?)'$/ do |one, two| 50 | File.read(file_path(one)).should == File.read(file_path(two)).reverse 51 | end 52 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 34 | When I retrieve the cache name '1369894322-345-1234-2255/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 | -------------------------------------------------------------------------------- /spec/uploader/proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | let(:test_file_name) { 'test.jpg' } 7 | let(:test_file) { File.open(file_path(test_file_name)) } 8 | let(:path) { '1369894322-345-1234-2255/test.jpeg' } 9 | 10 | after { FileUtils.rm_rf(public_path) } 11 | 12 | describe '#blank?' do 13 | subject { uploader } 14 | 15 | context "when nothing has been done" do 16 | it { is_expected.to be_blank } 17 | end 18 | 19 | context "when file is empty" do 20 | before { uploader.retrieve_from_cache!(path) } 21 | 22 | it { is_expected.to be_blank } 23 | end 24 | 25 | context "when file has been cached" do 26 | before { uploader.cache!(test_file) } 27 | 28 | it { is_expected.not_to be_blank } 29 | end 30 | end 31 | 32 | describe '#read' do 33 | subject { uploader.read } 34 | 35 | describe "default behavior" do 36 | it { is_expected.to be nil } 37 | end 38 | 39 | context "when file is cached" do 40 | before { uploader.cache!(test_file) } 41 | 42 | it { is_expected.to eq("this is stuff") } 43 | end 44 | end 45 | 46 | describe '#size' do 47 | subject { uploader.size } 48 | 49 | describe "default behavior" do 50 | it { is_expected.to be 0 } 51 | end 52 | 53 | context "when file is cached" do 54 | before { uploader.cache!(test_file) } 55 | 56 | it { is_expected.to be 13 } 57 | end 58 | end 59 | 60 | describe '#content_type' do 61 | subject { uploader.content_type } 62 | 63 | context "when nothing has been done" do 64 | it { is_expected.to be_nil } 65 | end 66 | 67 | context "when the file has been cached" do 68 | before { uploader.cache!(test_file) } 69 | 70 | it { is_expected.to eq('image/jpeg') } 71 | end 72 | 73 | context "when the file is empty" do 74 | before { uploader.retrieve_from_cache!(path) } 75 | 76 | it { is_expected.to eq('image/jpeg') } 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/uploader/default_url_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | 7 | after { FileUtils.rm_rf(public_path) } 8 | 9 | describe 'with a default url' do 10 | before do 11 | uploader_class.class_eval do 12 | version :thumb 13 | def default_url 14 | ['http://someurl.example.com', version_name].compact.join('/') 15 | end 16 | end 17 | end 18 | 19 | describe '#blank?' do 20 | subject { uploader } 21 | 22 | it "is blank by default" do 23 | is_expected.to be_blank 24 | end 25 | end 26 | 27 | describe '#current_path' do 28 | subject { uploader.current_path } 29 | 30 | it { is_expected.to be_nil } 31 | end 32 | 33 | describe '#url' do 34 | let(:url_example) { "http://someurl.example.com" } 35 | 36 | it "returns the default url" do 37 | expect(uploader.url).to eq(url_example) 38 | end 39 | 40 | it "returns the default url with version when given" do 41 | expect(uploader.url(:thumb)).to eq("#{url_example}/thumb") 42 | end 43 | end 44 | 45 | describe '#cache!' do 46 | let(:cache_id) { '1369894322-345-1234-2255' } 47 | let(:file_name) { 'test.jpg' } 48 | 49 | subject { uploader } 50 | 51 | before do 52 | allow(CarrierWave).to receive(:generate_cache_id).and_return(cache_id) 53 | uploader.cache!(File.open(file_path(file_name))) 54 | end 55 | 56 | it "caches a file" do 57 | expect(uploader.file).to be_an_instance_of(CarrierWave::SanitizedFile) 58 | end 59 | 60 | it "is cached" do 61 | expect(uploader).to be_cached 62 | end 63 | 64 | it "isn't blank" do 65 | expect(uploader).not_to be_blank 66 | end 67 | 68 | it "sets the current_path" do 69 | expect(uploader.current_path).to eq(public_path("uploads/tmp/#{cache_id}/#{file_name}")) 70 | end 71 | 72 | it "sets the url" do 73 | expect(uploader.url).to eq ("/uploads/tmp/#{cache_id}/#{file_name}") 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 35 | When I retrieve the cache name '1369894322-345-1234-2255/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/1369894322-345-1234-2255/bork.txt' 35 | When I retrieve the cache name '1369894322-345-1234-2255/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 | -------------------------------------------------------------------------------- /spec/uploader/remove_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | let(:file_name) { 'test.jpg' } 7 | let(:file) { File.open(file_path(file_name)) } 8 | 9 | after { FileUtils.rm_rf(public_path) } 10 | 11 | describe '#remove!' do 12 | let(:stored_file) { double('a stored file') } 13 | 14 | before do 15 | allow(CarrierWave).to receive(:generate_cache_id).and_return('1390890634-26112-1234-2122') 16 | 17 | cached_file = double('a cached file') 18 | allow(cached_file).to receive(:delete) 19 | 20 | allow(stored_file).to receive(:path).and_return('/path/to/somewhere') 21 | allow(stored_file).to receive(:url).and_return('http://www.example.com') 22 | allow(stored_file).to receive(:identifier).and_return('this-is-me') 23 | allow(stored_file).to receive(:delete) 24 | 25 | storage = double('a storage engine') 26 | allow(storage).to receive(:store!).and_return(stored_file) 27 | allow(storage).to receive(:cache!).and_return(cached_file) 28 | allow(storage).to receive(:delete_dir!).with("uploads/tmp/#{CarrierWave.generate_cache_id}") 29 | 30 | allow(uploader_class.storage).to receive(:new).and_return(storage) 31 | uploader.store!(file) 32 | end 33 | 34 | it "resets the current path" do 35 | uploader.remove! 36 | expect(uploader.current_path).to be_nil 37 | end 38 | 39 | it "should not be cached" do 40 | uploader.remove! 41 | expect(uploader).not_to be_cached 42 | end 43 | 44 | it "resets the url" do 45 | uploader.cache!(file) 46 | uploader.remove! 47 | expect(uploader.url).to be_nil 48 | end 49 | 50 | it "resets the identifier" do 51 | uploader.remove! 52 | expect(uploader.identifier).to be_nil 53 | end 54 | 55 | it "deletes the file" do 56 | expect(stored_file).to receive(:delete) 57 | uploader.remove! 58 | end 59 | 60 | it "resets the cache_name" do 61 | uploader.cache!(file) 62 | uploader.remove! 63 | expect(uploader.cache_name).to be_nil 64 | end 65 | 66 | it "does nothing when trying to remove an empty file" do 67 | expect{ uploader.remove! }.not_to raise_error 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /lib/carrierwave.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'active_support/core_ext/object/blank' 3 | require 'active_support/core_ext/object/try' 4 | require 'active_support/core_ext/class/attribute' 5 | require 'active_support/concern' 6 | 7 | module CarrierWave 8 | 9 | class << self 10 | attr_accessor :root, :base_path 11 | attr_writer :tmp_path 12 | 13 | def configure(&block) 14 | CarrierWave::Uploader::Base.configure(&block) 15 | end 16 | 17 | def clean_cached_files!(seconds=60*60*24) 18 | CarrierWave::Uploader::Base.clean_cached_files!(seconds) 19 | end 20 | 21 | def tmp_path 22 | @tmp_path ||= File.expand_path(File.join('..', 'tmp'), root) 23 | end 24 | end 25 | 26 | end 27 | 28 | if defined?(Merb) 29 | 30 | CarrierWave.root = Merb.dir_for(:public) 31 | Merb::BootLoader.before_app_loads do 32 | # Setup path for uploaders and load all of them before classes are loaded 33 | Merb.push_path(:uploaders, Merb.root / 'app' / 'uploaders', '*.rb') 34 | Dir.glob(File.join(Merb.load_paths[:uploaders])).each {|f| require f } 35 | end 36 | 37 | elsif defined?(Rails) 38 | 39 | module CarrierWave 40 | class Railtie < Rails::Railtie 41 | initializer "carrierwave.setup_paths" do |app| 42 | CarrierWave.root = Rails.root.join(Rails.public_path).to_s 43 | CarrierWave.base_path = ENV['RAILS_RELATIVE_URL_ROOT'] 44 | end 45 | 46 | initializer "carrierwave.active_record" do 47 | ActiveSupport.on_load :active_record do 48 | require 'carrierwave/orm/activerecord' 49 | end 50 | end 51 | end 52 | end 53 | 54 | elsif defined?(Sinatra) 55 | if defined?(Padrino) && defined?(PADRINO_ROOT) 56 | CarrierWave.root = File.join(PADRINO_ROOT, "public") 57 | else 58 | 59 | CarrierWave.root = if Sinatra::Application.respond_to?(:public_folder) 60 | # Sinatra >= 1.3 61 | Sinatra::Application.public_folder 62 | else 63 | # Sinatra < 1.3 64 | Sinatra::Application.public 65 | end 66 | end 67 | end 68 | 69 | require "carrierwave/utilities" 70 | require "carrierwave/error" 71 | require "carrierwave/sanitized_file" 72 | require "carrierwave/mounter" 73 | require "carrierwave/mount" 74 | require "carrierwave/processing" 75 | require "carrierwave/version" 76 | require "carrierwave/storage" 77 | require "carrierwave/uploader" 78 | require "carrierwave/compatibility/paperclip" 79 | require "carrierwave/test/matchers" 80 | -------------------------------------------------------------------------------- /features/versions_caching_from_versions.feature: -------------------------------------------------------------------------------- 1 | Feature: uploader with file storage and versions with overridden store dir 2 | In order to be awesome 3 | As a developer using CarrierWave 4 | I want to upload files to the filesystem 5 | 6 | Background: 7 | Given an uploader class that uses the 'file' storage 8 | Given a processor method named :upcase 9 | And that the uploader class has a version named 'thumb' which process 'upcase' 10 | And that the version 'thumb' has the store_dir overridden to 'public/monkey/llama' 11 | And that the uploader class has a version named 'small_thumb' which is based on version 'thumb' 12 | And that the version 'small_thumb' has the store_dir overridden to 'public/monkey/toro' 13 | And an instance of that class 14 | 15 | Scenario: cache a file and then store it 16 | When I cache the file 'fixtures/bork.txt' 17 | Then there should be a file called 'bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 18 | Then there should be a file called 'thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 19 | Then there should be a file called 'small_thumb_bork.txt' somewhere in a subdirectory of 'public/uploads/tmp' 20 | And the file called 'bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/bork.txt' 21 | And the file called 'thumb_bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/upcased_bork.txt' 22 | And the file called 'small_thumb_bork.txt' in a subdirectory of 'public/uploads/tmp' should be identical to the file at 'fixtures/upcased_bork.txt' 23 | And there should not be a file at 'public/uploads/bork.txt' 24 | And there should not be a file at 'public/monkey/llama/thumb_bork.txt' 25 | And there should not be a file at 'public/monkey/toro/small_thumb_bork.txt' 26 | When I store the file 27 | Then there should be a file at 'public/uploads/bork.txt' 28 | Then there should be a file at 'public/monkey/llama/thumb_bork.txt' 29 | Then there should be a file at 'public/monkey/toro/small_thumb_bork.txt' 30 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 31 | And the file at 'public/monkey/llama/thumb_bork.txt' should be identical to the file at 'fixtures/upcased_bork.txt' 32 | And the file at 'public/monkey/toro/small_thumb_bork.txt' should be identical to the file at 'fixtures/upcased_bork.txt' -------------------------------------------------------------------------------- /spec/uploader/content_type_blacklist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | let(:ruby_file) { File.open(file_path('ruby.gif')) } 7 | let(:bork_file) { File.open(file_path('bork.txt')) } 8 | let(:test_file) { File.open(file_path('test.jpeg')) } 9 | 10 | after { FileUtils.rm_rf(public_path) } 11 | 12 | describe '#cache!' do 13 | before do 14 | allow(CarrierWave).to receive(:generate_cache_id).and_return('1369894322-345-1234-2255') 15 | end 16 | 17 | context "when there is no blacklist" do 18 | it "does not raise an integrity error" do 19 | allow(uploader).to receive(:content_type_blacklist).and_return(nil) 20 | 21 | expect { uploader.cache!(ruby_file) }.not_to raise_error 22 | end 23 | end 24 | 25 | context "when there is a blacklist" do 26 | context "when the blacklist is an array of values" do 27 | it "does not raise an integrity error when the file has not a blacklisted content type" do 28 | allow(uploader).to receive(:content_type_blacklist).and_return(['image/gif']) 29 | 30 | expect { uploader.cache!(bork_file) }.not_to raise_error 31 | end 32 | 33 | it "raises an integrity error if the file has a blacklisted content type" do 34 | allow(uploader).to receive(:content_type_blacklist).and_return(['image/gif']) 35 | 36 | expect { uploader.cache!(ruby_file) }.to raise_error(CarrierWave::IntegrityError) 37 | end 38 | 39 | it "accepts content types as regular expressions" do 40 | allow(uploader).to receive(:content_type_blacklist).and_return([/image\//]) 41 | 42 | expect { uploader.cache!(ruby_file) }.to raise_error(CarrierWave::IntegrityError) 43 | end 44 | end 45 | 46 | context "when the blacklist is a single value" do 47 | it "accepts a single extension string value" do 48 | allow(uploader).to receive(:extension_whitelist).and_return('jpeg') 49 | 50 | expect { uploader.cache!(test_file) }.not_to raise_error 51 | end 52 | 53 | it "accepts a single extension regular expression value" do 54 | allow(uploader).to receive(:extension_whitelist).and_return(/jpe?g/) 55 | 56 | expect { uploader.cache!(test_file) }.not_to raise_error 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/uploader/content_type_whitelist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | let(:ruby_file) { File.open(file_path('ruby.gif')) } 7 | 8 | after { FileUtils.rm_rf(public_path) } 9 | 10 | describe '#cache!' do 11 | before do 12 | allow(CarrierWave).to receive(:generate_cache_id).and_return('1369894322-345-1234-2255') 13 | end 14 | 15 | context "when there is no whitelist" do 16 | it "does not raise an integrity error" do 17 | allow(uploader).to receive(:content_type_whitelist).and_return(nil) 18 | 19 | expect { uploader.cache!(ruby_file) }.not_to raise_error 20 | end 21 | end 22 | 23 | context "when there is a whitelist" do 24 | context "when the whitelist is an array of values" do 25 | let(:bork_file) { File.open(file_path('bork.txt')) } 26 | 27 | it "does not raise an integrity error when the file has a whitelisted content type" do 28 | allow(uploader).to receive(:content_type_whitelist).and_return(['image/gif']) 29 | 30 | expect { uploader.cache!(ruby_file) }.not_to raise_error 31 | end 32 | 33 | it "raises an integrity error the file has not a whitelisted content type" do 34 | allow(uploader).to receive(:content_type_whitelist).and_return(['image/gif']) 35 | 36 | expect { uploader.cache!(bork_file) }.to raise_error(CarrierWave::IntegrityError) 37 | end 38 | 39 | it "accepts content types as regular expressions" do 40 | allow(uploader).to receive(:content_type_whitelist).and_return([/image\//]) 41 | 42 | expect { uploader.cache!(bork_file) }.to raise_error(CarrierWave::IntegrityError) 43 | end 44 | end 45 | 46 | context "when the whitelist is a single value" do 47 | let(:test_file) { File.open(file_path('test.jpeg')) } 48 | 49 | it "accepts a single extension string value" do 50 | allow(uploader).to receive(:extension_whitelist).and_return('jpeg') 51 | 52 | expect { uploader.cache!(test_file) }.not_to raise_error 53 | end 54 | 55 | it "accepts a single extension regular expression value" do 56 | allow(uploader).to receive(:extension_whitelist).and_return(/jpe?g/) 57 | 58 | expect { uploader.cache!(test_file) }.not_to raise_error 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 40 | When I retrieve the cache name '1369894322-345-1234-2255/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.rb: -------------------------------------------------------------------------------- 1 | require "carrierwave/uploader/configuration" 2 | require "carrierwave/uploader/callbacks" 3 | require "carrierwave/uploader/proxy" 4 | require "carrierwave/uploader/url" 5 | require "carrierwave/uploader/mountable" 6 | require "carrierwave/uploader/cache" 7 | require "carrierwave/uploader/store" 8 | require "carrierwave/uploader/download" 9 | require "carrierwave/uploader/remove" 10 | require "carrierwave/uploader/extension_whitelist" 11 | require "carrierwave/uploader/extension_blacklist" 12 | require "carrierwave/uploader/content_type_whitelist" 13 | require "carrierwave/uploader/content_type_blacklist" 14 | require "carrierwave/uploader/file_size" 15 | require "carrierwave/uploader/processing" 16 | require "carrierwave/uploader/versions" 17 | require "carrierwave/uploader/default_url" 18 | 19 | require "carrierwave/uploader/serialization" 20 | 21 | module CarrierWave 22 | 23 | ## 24 | # See CarrierWave::Uploader::Base 25 | # 26 | module Uploader 27 | 28 | ## 29 | # An uploader is a class that allows you to easily handle the caching and storage of 30 | # uploaded files. Please refer to the README for configuration options. 31 | # 32 | # Once you have an uploader you can use it in isolation: 33 | # 34 | # my_uploader = MyUploader.new 35 | # my_uploader.cache!(File.open(path_to_file)) 36 | # my_uploader.retrieve_from_store!('monkey.png') 37 | # 38 | # Alternatively, you can mount it on an ORM or other persistence layer, with 39 | # +CarrierWave::Mount#mount_uploader+. There are extensions for activerecord and datamapper 40 | # these are *very* simple (they are only a dozen lines of code), so adding your own should 41 | # be trivial. 42 | # 43 | class Base 44 | attr_reader :file 45 | 46 | include CarrierWave::Uploader::Configuration 47 | include CarrierWave::Uploader::Callbacks 48 | include CarrierWave::Uploader::Proxy 49 | include CarrierWave::Uploader::Url 50 | include CarrierWave::Uploader::Mountable 51 | include CarrierWave::Uploader::Cache 52 | include CarrierWave::Uploader::Store 53 | include CarrierWave::Uploader::Download 54 | include CarrierWave::Uploader::Remove 55 | include CarrierWave::Uploader::ExtensionWhitelist 56 | include CarrierWave::Uploader::ExtensionBlacklist 57 | include CarrierWave::Uploader::ContentTypeWhitelist 58 | include CarrierWave::Uploader::ContentTypeBlacklist 59 | include CarrierWave::Uploader::FileSize 60 | include CarrierWave::Uploader::Processing 61 | include CarrierWave::Uploader::Versions 62 | include CarrierWave::Uploader::DefaultUrl 63 | include CarrierWave::Uploader::Serialization 64 | end # Base 65 | 66 | end # Uploader 67 | end # CarrierWave 68 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 35 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/1369894322-345-1234-2255/thumb_bork.txt' 36 | When I retrieve the cache name '1369894322-345-1234-2255/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 | -------------------------------------------------------------------------------- /lib/carrierwave/validations/active_model.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/validator' 2 | require 'active_support/concern' 3 | 4 | module CarrierWave 5 | 6 | # == Active Model Presence Validator 7 | module Validations 8 | module ActiveModel 9 | extend ActiveSupport::Concern 10 | 11 | class ProcessingValidator < ::ActiveModel::EachValidator 12 | 13 | def validate_each(record, attribute, value) 14 | if e = record.__send__("#{attribute}_processing_error") 15 | message = (e.message == e.class.to_s) ? :carrierwave_processing_error : e.message 16 | record.errors.add(attribute, message) 17 | end 18 | end 19 | end 20 | 21 | class IntegrityValidator < ::ActiveModel::EachValidator 22 | 23 | def validate_each(record, attribute, value) 24 | if e = record.__send__("#{attribute}_integrity_error") 25 | message = (e.message == e.class.to_s) ? :carrierwave_integrity_error : e.message 26 | record.errors.add(attribute, message) 27 | end 28 | end 29 | end 30 | 31 | class DownloadValidator < ::ActiveModel::EachValidator 32 | 33 | def validate_each(record, attribute, value) 34 | if e = record.__send__("#{attribute}_download_error") 35 | message = (e.message == e.class.to_s) ? :carrierwave_download_error : e.message 36 | record.errors.add(attribute, message) 37 | end 38 | end 39 | end 40 | 41 | module HelperMethods 42 | 43 | ## 44 | # Makes the record invalid if the file couldn't be uploaded due to an integrity error 45 | # 46 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 47 | # 48 | def validates_integrity_of(*attr_names) 49 | validates_with IntegrityValidator, _merge_attributes(attr_names) 50 | end 51 | 52 | ## 53 | # Makes the record invalid if the file couldn't be processed (assuming the process failed 54 | # with a CarrierWave::ProcessingError) 55 | # 56 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 57 | # 58 | def validates_processing_of(*attr_names) 59 | validates_with ProcessingValidator, _merge_attributes(attr_names) 60 | end 61 | # 62 | ## 63 | # Makes the record invalid if the remote file couldn't be downloaded 64 | # 65 | # Accepts the usual parameters for validations in Rails (:if, :unless, etc...) 66 | # 67 | def validates_download_of(*attr_names) 68 | validates_with DownloadValidator, _merge_attributes(attr_names) 69 | end 70 | end 71 | 72 | included do 73 | extend HelperMethods 74 | include HelperMethods 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/magic_mime_blacklist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | 4 | ## 5 | # This modules validates the content type of a file with the use of 6 | # ruby-filemagic gem and a blacklist regular expression. If you want 7 | # to use this, you'll need to require this file: 8 | # 9 | # require 'carrierwave/uploader/magic_mime_blacklist' 10 | # 11 | # And then include it in your uploader: 12 | # 13 | # class MyUploader < CarrierWave::Uploader::Base 14 | # include CarrierWave::Uploader::MagicMimeBlacklist 15 | # 16 | # def blacklist_mime_type_pattern 17 | # /image\// 18 | # end 19 | # end 20 | # 21 | module MagicMimeBlacklist 22 | extend ActiveSupport::Concern 23 | 24 | included do 25 | begin 26 | require "filemagic" 27 | rescue LoadError => e 28 | e.message << " (You may need to install the ruby-filemagic gem)" 29 | raise e 30 | end 31 | 32 | before :cache, :check_blacklist_pattern! 33 | end 34 | 35 | ## 36 | # Override this method in your uploader to provide a black list pattern (regexp) 37 | # of content-types which are prohibited to be uploaded. 38 | # Compares the file's content-type. 39 | # 40 | # === Returns 41 | # 42 | # [Regexp] a black list regexp to match the content_type 43 | # 44 | # === Examples 45 | # 46 | # def blacklist_mime_type_pattern 47 | # /(text|application)\/json/ 48 | # end 49 | # 50 | def blacklist_mime_type_pattern; end 51 | 52 | private 53 | 54 | def check_blacklist_pattern!(new_file) 55 | return if blacklist_mime_type_pattern.nil? 56 | 57 | content_type = extract_content_type(new_file) 58 | 59 | if content_type.match(blacklist_mime_type_pattern) 60 | raise CarrierWave::IntegrityError, 61 | I18n.translate(:"errors.messages.mime_type_pattern_black_list_error", 62 | :content_type => content_type) 63 | end 64 | end 65 | 66 | ## 67 | # Extracts the content type of the given file 68 | # 69 | # === Returns 70 | # 71 | # [String] the extracted content type 72 | # 73 | def extract_content_type(new_file) 74 | content_type = nil 75 | 76 | File.open(new_file.path) do |fd| 77 | data = fd.read(1024) || "" 78 | content_type = filemagic.buffer(data) 79 | end 80 | 81 | content_type 82 | end 83 | 84 | ## 85 | # FileMagic object with the MAGIC_MIME_TYPE flag set 86 | # 87 | # @return [FileMagic] a filemagic object 88 | def filemagic 89 | @filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME_TYPE) 90 | end 91 | 92 | end # MagicMimeblackList 93 | end # Uploader 94 | end # CarrierWave 95 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/magic_mime_whitelist.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | 4 | ## 5 | # This modules validates the content type of a file with the use of 6 | # ruby-filemagic gem and a whitelist regular expression. If you want 7 | # to use this, you'll need to require this file: 8 | # 9 | # require 'carrierwave/uploader/magic_mime_whitelist' 10 | # 11 | # And then include it in your uploader: 12 | # 13 | # class MyUploader < CarrierWave::Uploader::Base 14 | # include CarrierWave::Uploader::MagicMimeWhitelist 15 | # 16 | # def whitelist_mime_type_pattern 17 | # /image\// 18 | # end 19 | # end 20 | # 21 | module MagicMimeWhitelist 22 | extend ActiveSupport::Concern 23 | 24 | included do 25 | begin 26 | require "filemagic" 27 | rescue LoadError => e 28 | e.message << " (You may need to install the ruby-filemagic gem)" 29 | raise e 30 | end 31 | 32 | before :cache, :check_whitelist_pattern! 33 | end 34 | 35 | ## 36 | # Override this method in your uploader to provide a white list pattern (regexp) 37 | # of content-types which are allowed to be uploaded. 38 | # Compares the file's content-type. 39 | # 40 | # === Returns 41 | # 42 | # [Regexp] a white list regexp to match the content_type 43 | # 44 | # === Examples 45 | # 46 | # def whitelist_mime_type_pattern 47 | # /(text|application)\/json/ 48 | # end 49 | # 50 | def whitelist_mime_type_pattern; end 51 | 52 | private 53 | 54 | def check_whitelist_pattern!(new_file) 55 | return if whitelist_mime_type_pattern.nil? 56 | 57 | content_type = extract_content_type(new_file) 58 | 59 | if !content_type.match(whitelist_mime_type_pattern) 60 | raise CarrierWave::IntegrityError, 61 | I18n.translate(:"errors.messages.mime_type_pattern_white_list_error", 62 | :content_type => content_type) 63 | end 64 | end 65 | 66 | ## 67 | # Extracts the content type of the given file 68 | # 69 | # === Returns 70 | # 71 | # [String] the extracted content type 72 | # 73 | def extract_content_type(new_file) 74 | content_type = nil 75 | 76 | File.open(new_file.path) do |fd| 77 | data = fd.read(1024) || "" 78 | content_type = filemagic.buffer(data) 79 | end 80 | 81 | content_type 82 | end 83 | 84 | ## 85 | # FileMagic object with the MAGIC_MIME_TYPE flag set 86 | # 87 | # @return [FileMagic] a filemagic object 88 | def filemagic 89 | @filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME_TYPE) 90 | end 91 | 92 | end # MagicMimeWhiteList 93 | end # Uploader 94 | end # CarrierWave 95 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/processing.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Processing 4 | extend ActiveSupport::Concern 5 | 6 | include CarrierWave::Uploader::Callbacks 7 | 8 | included do 9 | class_attribute :processors, :instance_writer => false 10 | self.processors = [] 11 | 12 | before :cache, :process! 13 | end 14 | 15 | module ClassMethods 16 | 17 | ## 18 | # Adds a processor callback which applies operations as a file is uploaded. 19 | # The argument may be the name of any method of the uploader, expressed as a symbol, 20 | # or a list of such methods, or a hash where the key is a method and the value is 21 | # an array of arguments to call the method with 22 | # 23 | # === Parameters 24 | # 25 | # args (*Symbol, Hash{Symbol => Array[]}) 26 | # 27 | # === Examples 28 | # 29 | # class MyUploader < CarrierWave::Uploader::Base 30 | # 31 | # process :sepiatone, :vignette 32 | # process :scale => [200, 200] 33 | # process :scale => [200, 200], :if => :image? 34 | # process :sepiatone, :if => :image? 35 | # 36 | # def sepiatone 37 | # ... 38 | # end 39 | # 40 | # def vignette 41 | # ... 42 | # end 43 | # 44 | # def scale(height, width) 45 | # ... 46 | # end 47 | # 48 | # def image? 49 | # ... 50 | # end 51 | # 52 | # end 53 | # 54 | def process(*args) 55 | new_processors = args.inject({}) do |hash, arg| 56 | arg = { arg => [] } unless arg.is_a?(Hash) 57 | hash.merge!(arg) 58 | end 59 | 60 | condition = new_processors.delete(:if) 61 | new_processors.each do |processor, processor_args| 62 | self.processors += [[processor, processor_args, condition]] 63 | end 64 | end 65 | 66 | end # ClassMethods 67 | 68 | ## 69 | # Apply all process callbacks added through CarrierWave.process 70 | # 71 | def process!(new_file=nil) 72 | return unless enable_processing 73 | 74 | with_callbacks(:process, new_file) do 75 | self.class.processors.each do |method, args, condition| 76 | if(condition) 77 | if condition.respond_to?(:call) 78 | next unless condition.call(self, :args => args, :method => method, :file => new_file) 79 | else 80 | next unless self.send(condition, new_file) 81 | end 82 | end 83 | self.send(method, *args) 84 | end 85 | end 86 | end 87 | 88 | end # Processing 89 | end # Uploader 90 | end # CarrierWave 91 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/store.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Uploader 3 | module Store 4 | extend ActiveSupport::Concern 5 | 6 | include CarrierWave::Uploader::Callbacks 7 | include CarrierWave::Uploader::Configuration 8 | include CarrierWave::Uploader::Cache 9 | 10 | ## 11 | # Override this in your Uploader to change the filename. 12 | # 13 | # Be careful using record ids as filenames. If the filename is stored in the database 14 | # the record id will be nil when the filename is set. Don't use record ids unless you 15 | # understand this limitation. 16 | # 17 | # Do not use the version_name in the filename, as it will prevent versions from being 18 | # loaded correctly. 19 | # 20 | # === Returns 21 | # 22 | # [String] a filename 23 | # 24 | def filename 25 | @filename 26 | end 27 | 28 | ## 29 | # Calculates the path where the file should be stored. If +for_file+ is given, it will be 30 | # used as the filename, otherwise +CarrierWave::Uploader#filename+ is assumed. 31 | # 32 | # === Parameters 33 | # 34 | # [for_file (String)] name of the file 35 | # 36 | # === Returns 37 | # 38 | # [String] the store path 39 | # 40 | def store_path(for_file=filename) 41 | File.join([store_dir, full_filename(for_file)].compact) 42 | end 43 | 44 | ## 45 | # Stores the file by passing it to this Uploader's storage engine. 46 | # 47 | # If new_file is omitted, a previously cached file will be stored. 48 | # 49 | # === Parameters 50 | # 51 | # [new_file (File, IOString, Tempfile)] any kind of file object 52 | # 53 | def store!(new_file=nil) 54 | cache!(new_file) if new_file && ((@cache_id != parent_cache_id) || @cache_id.nil?) 55 | if !cache_only and @file and @cache_id 56 | with_callbacks(:store, new_file) do 57 | new_file = storage.store!(@file) 58 | if delete_tmp_file_after_storage 59 | @file.delete unless move_to_store 60 | cache_storage.delete_dir!(cache_path(nil)) 61 | end 62 | @file = new_file 63 | @cache_id = nil 64 | end 65 | end 66 | end 67 | 68 | ## 69 | # Retrieves the file from the storage. 70 | # 71 | # === Parameters 72 | # 73 | # [identifier (String)] uniquely identifies the file to retrieve 74 | # 75 | def retrieve_from_store!(identifier) 76 | with_callbacks(:retrieve_from_store, identifier) do 77 | @file = storage.retrieve!(identifier) 78 | end 79 | end 80 | 81 | private 82 | 83 | def full_filename(for_file) 84 | for_file 85 | end 86 | 87 | def storage 88 | @storage ||= self.class.storage.new(self) 89 | end 90 | 91 | end # Store 92 | end # Uploader 93 | end # CarrierWave 94 | -------------------------------------------------------------------------------- /spec/uploader/overrides_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) do 5 | Class.new(CarrierWave::Uploader::Base).tap do |uc| 6 | uc.configure do |config| 7 | 8 | config.fog_credentials = { 9 | :provider => 'AWS', # required 10 | :aws_access_key_id => 'XXXX', # required 11 | :aws_secret_access_key => 'YYYY', # required 12 | :region => 'us-east-1' # optional, defaults to 'us-east-1' 13 | } 14 | 15 | config.fog_directory = "defaultbucket" 16 | end 17 | end 18 | end 19 | 20 | let(:uploader) { uploader_class.new } 21 | 22 | let(:uploader_overridden) do 23 | uploader_class.tap do |uo| 24 | uo.fog_credentials = { 25 | :provider => 'AWS', # required 26 | :aws_access_key_id => 'ZZZZ', # required 27 | :aws_secret_access_key => 'AAAA', # required 28 | :region => 'us-east-2' # optional, defaults to 'us-east-1' 29 | } 30 | uo.fog_public = false 31 | end 32 | end 33 | 34 | describe 'fog_credentials' do 35 | describe 'reflects the standard value if no override done' do 36 | it { expect(uploader.fog_credentials).to be_a(Hash) } 37 | it { expect(uploader.fog_credentials[:provider]).to be_eql('AWS') } 38 | it { expect(uploader.fog_credentials[:aws_access_key_id]).to be_eql('XXXX') } 39 | it { expect(uploader.fog_credentials[:aws_secret_access_key]).to be_eql('YYYY') } 40 | it { expect(uploader.fog_credentials[:region]).to be_eql('us-east-1') } 41 | end 42 | 43 | describe 'reflects the new values in uploader class with override' do 44 | it { expect(uploader_overridden.fog_credentials).to be_a(Hash) } 45 | it { expect(uploader_overridden.fog_credentials[:provider]).to be_eql('AWS') } 46 | it { expect(uploader_overridden.fog_credentials[:aws_access_key_id]).to be_eql('ZZZZ') } 47 | it { expect(uploader_overridden.fog_credentials[:aws_secret_access_key]).to be_eql('AAAA') } 48 | it { expect(uploader_overridden.fog_credentials[:region]).to be_eql('us-east-2') } 49 | end 50 | end 51 | 52 | describe 'fog_directory' do 53 | it 'reflects the standard value if no override done' do 54 | expect(uploader.fog_directory).to be_eql('defaultbucket') 55 | end 56 | 57 | it 'reflects the standard value in overridden object because property is not overridden' do 58 | expect(uploader_overridden.fog_directory).to be_eql('defaultbucket') 59 | end 60 | end 61 | 62 | describe 'fog_public' do 63 | it 'reflects the standard value if no override done' do 64 | expect(uploader.fog_public).to be_eql(true) 65 | end 66 | 67 | it 'reflects the standard value in overridden object because property is not overridden' do 68 | expect(uploader_overridden.fog_public).to be_eql(false) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 37 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/1369894322-345-1234-2255/thumb_bork.txt' 38 | When I retrieve the cache name '1369894322-345-1234-2255/bork.txt' from the cache 39 | And I store the file 40 | Then there should be a file at 'public/uploads/bork.txt' 41 | Then there should be a file at 'public/uploads/thumb_bork.txt' 42 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 43 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 44 | 45 | Scenario: retrieving a file from store 46 | Given the file 'fixtures/bork.txt' is stored at 'public/uploads/bork.txt' 47 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_bork.txt' 48 | When I retrieve the file 'bork.txt' from the store 49 | Then the uploader should have the url '/uploads/bork.txt' 50 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 51 | -------------------------------------------------------------------------------- /spec/storage/file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/file_utils_helper' 3 | require 'tempfile' 4 | 5 | describe CarrierWave::Storage::File do 6 | include FileUtilsHelper 7 | 8 | subject(:storage) { described_class.new(uploader) } 9 | 10 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 11 | let(:tempfile) { Tempfile.new("foo") } 12 | let(:sanitized_temp_file) { CarrierWave::SanitizedFile.new(tempfile) } 13 | let(:uploader) { uploader_class.new } 14 | 15 | after { FileUtils.rm_rf(public_path) } 16 | 17 | describe '#delete_dir!' do 18 | let(:file) { File.open(file_path("test.jpg")) } 19 | 20 | context "when the directory is not empty" do 21 | let(:cache_id_dir) { File.dirname(cache_path) } 22 | let(:cache_path) { File.expand_path(File.join(uploader.cache_dir, uploader.cache_name), uploader.root) } 23 | let(:existing_file) { File.join(cache_id_dir, "exsting_file.txt") } 24 | 25 | before do 26 | uploader.cache!(file) 27 | File.open(existing_file, "wb"){|f| f << "I exist"} 28 | uploader.store! 29 | end 30 | 31 | it "doesn't delete the old cache_id" do 32 | expect(File).to be_directory(cache_id_dir) 33 | end 34 | 35 | it "doesn't delete other existing files in old cache_id dir" do 36 | expect(File).to exist existing_file 37 | end 38 | end 39 | end 40 | 41 | describe '#cache!' do 42 | context "when FileUtils.mkdir_p raises Errno::EMLINK" do 43 | before { fake_failed_mkdir_p } 44 | after { storage.cache!(sanitized_temp_file) } 45 | 46 | it { is_expected.to receive(:clean_cache!).with(600) } 47 | end 48 | end 49 | 50 | describe '#clean_cache!' do 51 | let(:today) { '2016/10/09 10:00:00'.to_time } 52 | let(:five_days_ago) { today.ago(5.days) } 53 | let(:three_days_ago) { today.ago(3.days) } 54 | let(:yesterday) { today.yesterday } 55 | let(:cache_dir) { File.expand_path(uploader_class.cache_dir, CarrierWave.root) } 56 | 57 | before do 58 | [five_days_ago, three_days_ago, yesterday, (today - 1.minute)].each do |created_date| 59 | Timecop.freeze (created_date) do 60 | FileUtils.mkdir_p File.expand_path(CarrierWave.generate_cache_id, cache_dir) 61 | end 62 | end 63 | end 64 | 65 | after { FileUtils.rm_rf(cache_dir) } 66 | 67 | it "clears all files older than now in the default cache directory" do 68 | Timecop.freeze(today) { uploader_class.clean_cached_files!(0) } 69 | 70 | expect(Dir.glob("#{cache_dir}/*").size).to eq(0) 71 | end 72 | 73 | it "clears all files older than, by default, 24 hours in the default cache directory" do 74 | Timecop.freeze(today) { uploader_class.clean_cached_files! } 75 | 76 | expect(Dir.glob("#{cache_dir}/*").size).to eq(2) 77 | end 78 | 79 | it "allows to set since how many seconds delete the cached files" do 80 | Timecop.freeze(today) { uploader_class.clean_cached_files!(4.days) } 81 | 82 | expect(Dir.glob("#{cache_dir}/*").size).to eq(3) 83 | end 84 | 85 | it "'s aliased on the CarrierWave module" do 86 | Timecop.freeze(today) { CarrierWave.clean_cached_files! } 87 | 88 | expect(Dir.glob("#{cache_dir}/*").size).to eq(2) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /features/step_definitions/general_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^an uploader class that uses the '(.*?)' storage$/ do |kind| 2 | @klass = Class.new(CarrierWave::Uploader::Base) 3 | @klass.storage = kind.to_sym 4 | end 5 | 6 | Given /^an instance of that class$/ do 7 | @uploader = @klass.new 8 | end 9 | 10 | Given /^a processor method named :upcase$/ do 11 | @klass.class_eval do 12 | define_method(:upcase) do 13 | content = File.read(current_path) 14 | File.open(current_path, 'w') { |f| f.write content.upcase } 15 | end 16 | end 17 | end 18 | 19 | Then /^the contents of the file should be '(.*?)'$/ do |contents| 20 | @uploader.read.chomp.should == contents 21 | end 22 | 23 | Given /^that the uploader reverses the filename$/ do 24 | @klass.class_eval do 25 | def filename 26 | super.reverse unless super.blank? 27 | end 28 | end 29 | end 30 | 31 | Given /^that the uploader has the filename overridden to '(.*?)'$/ do |filename| 32 | @klass.class_eval do 33 | define_method(:filename) do 34 | filename 35 | end 36 | end 37 | end 38 | 39 | Given /^that the uploader has the store_dir overridden to '(.*?)'$/ do |store_dir| 40 | @klass.class_eval do 41 | define_method(:store_dir) do 42 | file_path(store_dir) 43 | end 44 | end 45 | end 46 | 47 | Given /^that the version '(.*?)' has the store_dir overridden to '(.*?)'$/ do |version, store_dir| 48 | @klass.versions[version.to_sym].class_eval do 49 | define_method(:store_dir) do 50 | file_path(store_dir) 51 | end 52 | end 53 | end 54 | 55 | Given /^that the uploader class has a version named '([^\']+)'$/ do |name| 56 | @klass.version(name) 57 | end 58 | 59 | Given /^that the uploader class has a version named '([^\']+)' which process '([a-zA-Z0-9\_\?!]*)'$/ do |name, processor_name| 60 | @klass.version(name) do 61 | process processor_name.to_sym 62 | end 63 | end 64 | 65 | Given /^that the uploader class has a version named '([^\']+)' which is based on version '(.*?)'$/ do |name, based_version_name| 66 | @klass.version(name, {:from_version => based_version_name.to_sym}) 67 | end 68 | 69 | Given /^yo dawg, I put a version called '(.*?)' in your version called '(.*?)'$/ do |v2, v1| 70 | @klass.version(v1) do 71 | version(v2) 72 | end 73 | end 74 | 75 | Given /^the class has a method called 'reverse' that reverses the contents of a file$/ do 76 | @klass.class_eval do 77 | def reverse 78 | text = File.read(current_path) 79 | File.open(current_path, 'w') { |f| f.write(text.reverse) } 80 | end 81 | end 82 | end 83 | 84 | Given /^the class will process '([a-zA-Z0-9\_\?!]*)'$/ do |name| 85 | @klass.process name.to_sym 86 | end 87 | 88 | Then /^the uploader should have '(.*?)' as its current path$/ do |path| 89 | @uploader.current_path.should == file_path(path) 90 | end 91 | 92 | Then /^the uploader should have the url '(.*?)'$/ do |url| 93 | @uploader.url.should == url 94 | end 95 | 96 | Then /^the uploader's version '(.*?)' should have the url '(.*?)'$/ do |version, url| 97 | @uploader.versions[version.to_sym].url.should == url 98 | end 99 | 100 | Then /^the uploader's nested version '(.*?)' nested in '(.*?)' should have the url '(.*?)'$/ do |v2, v1, url| 101 | @uploader.versions[v1.to_sym].versions[v2.to_sym].url.should == url 102 | end 103 | -------------------------------------------------------------------------------- /lib/carrierwave/orm/activerecord.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'carrierwave/validations/active_model' 3 | 4 | module CarrierWave 5 | module ActiveRecord 6 | 7 | include CarrierWave::Mount 8 | 9 | ## 10 | # See +CarrierWave::Mount#mount_uploader+ for documentation 11 | # 12 | def mount_uploader(column, uploader=nil, options={}, &block) 13 | super 14 | 15 | class_eval <<-RUBY, __FILE__, __LINE__+1 16 | def remote_#{column}_url=(url) 17 | column = _mounter(:#{column}).serialization_column 18 | __send__(:"\#{column}_will_change!") 19 | super 20 | end 21 | RUBY 22 | end 23 | 24 | ## 25 | # See +CarrierWave::Mount#mount_uploaders+ for documentation 26 | # 27 | def mount_uploaders(column, uploader=nil, options={}, &block) 28 | super 29 | 30 | class_eval <<-RUBY, __FILE__, __LINE__+1 31 | def remote_#{column}_urls=(url) 32 | column = _mounter(:#{column}).serialization_column 33 | __send__(:"\#{column}_will_change!") 34 | super 35 | end 36 | RUBY 37 | end 38 | 39 | private 40 | 41 | def mount_base(column, uploader=nil, options={}, &block) 42 | super 43 | 44 | alias_method :read_uploader, :read_attribute 45 | alias_method :write_uploader, :write_attribute 46 | public :read_uploader 47 | public :write_uploader 48 | 49 | include CarrierWave::Validations::ActiveModel 50 | 51 | validates_integrity_of column if uploader_option(column.to_sym, :validate_integrity) 52 | validates_processing_of column if uploader_option(column.to_sym, :validate_processing) 53 | validates_download_of column if uploader_option(column.to_sym, :validate_download) 54 | 55 | after_save :"store_#{column}!" 56 | before_save :"write_#{column}_identifier" 57 | after_commit :"remove_#{column}!", :on => :destroy 58 | after_commit :"mark_remove_#{column}_false", :on => :update 59 | 60 | after_save :"store_previous_changes_for_#{column}" 61 | after_commit :"remove_previously_stored_#{column}", :on => :update 62 | 63 | class_eval <<-RUBY, __FILE__, __LINE__+1 64 | def #{column}=(new_file) 65 | column = _mounter(:#{column}).serialization_column 66 | if !(new_file.blank? && __send__(:#{column}).blank?) 67 | __send__(:"\#{column}_will_change!") 68 | end 69 | 70 | super 71 | end 72 | 73 | def remove_#{column}=(value) 74 | column = _mounter(:#{column}).serialization_column 75 | __send__(:"\#{column}_will_change!") 76 | super 77 | end 78 | 79 | def remove_#{column}! 80 | self.remove_#{column} = true 81 | write_#{column}_identifier 82 | self.remove_#{column} = false 83 | super 84 | end 85 | 86 | # Reset cached mounter on record reload 87 | def reload(*) 88 | @_mounters = nil 89 | super 90 | end 91 | 92 | # Reset cached mounter on record dup 93 | def initialize_dup(other) 94 | @_mounters = nil 95 | super 96 | end 97 | RUBY 98 | end 99 | 100 | end # ActiveRecord 101 | end # CarrierWave 102 | 103 | ActiveRecord::Base.extend CarrierWave::ActiveRecord 104 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 38 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/1369894322-345-1234-2255/thumb_bork.txt' 39 | When I retrieve the cache name '1369894322-345-1234-2255/bork.txt' from the cache 40 | And I store the file 41 | Then there should be a file at 'public/uploads/grark.png' 42 | Then there should be a file at 'public/uploads/thumb_grark.png' 43 | And the file at 'public/uploads/grark.png' should be identical to the file at 'fixtures/bork.txt' 44 | And the file at 'public/uploads/thumb_grark.png' should be identical to the file at 'fixtures/monkey.txt' 45 | 46 | Scenario: retrieving a file from store 47 | Given the file 'fixtures/bork.txt' is stored at 'public/uploads/bork.txt' 48 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_bork.txt' 49 | When I retrieve the file 'bork.txt' from the store 50 | Then the uploader should have the url '/uploads/bork.txt' 51 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 52 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/download.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module CarrierWave 4 | module Uploader 5 | module Download 6 | extend ActiveSupport::Concern 7 | 8 | include CarrierWave::Uploader::Callbacks 9 | include CarrierWave::Uploader::Configuration 10 | include CarrierWave::Uploader::Cache 11 | 12 | class RemoteFile 13 | def initialize(uri, remote_headers = {}) 14 | @uri = uri 15 | @remote_headers = remote_headers 16 | end 17 | 18 | def original_filename 19 | filename = filename_from_header || filename_from_uri 20 | mime_type = MIME::Types[file.content_type].first 21 | unless File.extname(filename).present? || mime_type.blank? 22 | filename = "#{filename}.#{mime_type.extensions.first}" 23 | end 24 | filename 25 | end 26 | 27 | def respond_to?(*args) 28 | super or file.respond_to?(*args) 29 | end 30 | 31 | def http? 32 | @uri.scheme =~ /^https?$/ 33 | end 34 | 35 | private 36 | 37 | def file 38 | if @file.blank? 39 | headers = @remote_headers. 40 | reverse_merge('User-Agent' => "CarrierWave/#{CarrierWave::VERSION}") 41 | 42 | @file = Kernel.open(@uri.to_s, headers) 43 | @file = @file.is_a?(String) ? StringIO.new(@file) : @file 44 | end 45 | @file 46 | 47 | rescue StandardError => e 48 | raise CarrierWave::DownloadError, "could not download file: #{e.message}" 49 | end 50 | 51 | def filename_from_header 52 | if file.meta.include? 'content-disposition' 53 | match = file.meta['content-disposition'].match(/filename="?([^"]+)/) 54 | return match[1] unless match.nil? || match[1].empty? 55 | end 56 | end 57 | 58 | def filename_from_uri 59 | URI.decode(File.basename(file.base_uri.path)) 60 | end 61 | 62 | def method_missing(*args, &block) 63 | file.send(*args, &block) 64 | end 65 | end 66 | 67 | ## 68 | # Caches the file by downloading it from the given URL. 69 | # 70 | # === Parameters 71 | # 72 | # [url (String)] The URL where the remote file is stored 73 | # [remote_headers (Hash)] Request headers 74 | # 75 | def download!(uri, remote_headers = {}) 76 | processed_uri = process_uri(uri) 77 | file = RemoteFile.new(processed_uri, remote_headers) 78 | raise CarrierWave::DownloadError, "trying to download a file which is not served over HTTP" unless file.http? 79 | cache!(file) 80 | end 81 | 82 | ## 83 | # Processes the given URL by parsing and escaping it. Public to allow overriding. 84 | # 85 | # === Parameters 86 | # 87 | # [url (String)] The URL where the remote file is stored 88 | # 89 | def process_uri(uri) 90 | URI.parse(uri) 91 | rescue URI::InvalidURIError 92 | uri_parts = uri.split('?') 93 | # regexp from Ruby's URI::Parser#regexp[:UNSAFE], with [] specifically removed 94 | encoded_uri = URI.encode(uri_parts.shift, /[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,]/) 95 | encoded_uri << '?' << URI.encode(uri_parts.join('?')) if uri_parts.any? 96 | URI.parse(encoded_uri) rescue raise CarrierWave::DownloadError, "couldn't parse URL" 97 | end 98 | 99 | end # Download 100 | end # Uploader 101 | end # CarrierWave 102 | -------------------------------------------------------------------------------- /lib/carrierwave/compatibility/paperclip.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Compatibility 3 | 4 | ## 5 | # Mix this module into an Uploader to make it mimic Paperclip's storage paths 6 | # This will make your Uploader use the same default storage path as paperclip 7 | # does. If you need to override it, you can override the +paperclip_path+ method 8 | # and provide a Paperclip style path: 9 | # 10 | # class MyUploader < CarrierWave::Uploader::Base 11 | # include CarrierWave::Compatibility::Paperclip 12 | # 13 | # def paperclip_path 14 | # ":rails_root/public/uploads/:id/:attachment/:style_:basename.:extension" 15 | # end 16 | # end 17 | # 18 | # --- 19 | # 20 | # This file contains code taken from Paperclip 21 | # 22 | # LICENSE 23 | # 24 | # The MIT License 25 | # 26 | # Copyright (c) 2008 Jon Yurek and thoughtbot, inc. 27 | # 28 | # Permission is hereby granted, free of charge, to any person obtaining a copy 29 | # of this software and associated documentation files (the "Software"), to deal 30 | # in the Software without restriction, including without limitation the rights 31 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | # copies of the Software, and to permit persons to whom the Software is 33 | # furnished to do so, subject to the following conditions: 34 | # 35 | # The above copyright notice and this permission notice shall be included in 36 | # all copies or substantial portions of the Software. 37 | # 38 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 44 | # THE SOFTWARE. 45 | # 46 | module Paperclip 47 | extend ActiveSupport::Concern 48 | 49 | DEFAULT_MAPPINGS = { 50 | :rails_root => lambda{|u, f| Rails.root.to_s }, 51 | :rails_env => lambda{|u, f| Rails.env }, 52 | :id_partition => lambda{|u, f| ("%09d" % u.model.id).scan(/\d{3}/).join("/")}, 53 | :id => lambda{|u, f| u.model.id }, 54 | :attachment => lambda{|u, f| u.mounted_as.to_s.downcase.pluralize }, 55 | :style => lambda{|u, f| u.paperclip_style }, 56 | :basename => lambda{|u, f| u.filename.gsub(/#{File.extname(u.filename)}$/, "") }, 57 | :extension => lambda{|u, d| File.extname(u.filename).gsub(/^\.+/, "")}, 58 | :class => lambda{|u, f| u.model.class.name.underscore.pluralize} 59 | } 60 | 61 | included do 62 | attr_accessor :filename 63 | class_attribute :mappings 64 | self.mappings ||= DEFAULT_MAPPINGS.dup 65 | end 66 | 67 | def store_path(for_file=filename) 68 | path = paperclip_path 69 | self.filename = for_file 70 | path ||= File.join(*[store_dir, paperclip_style.to_s, for_file].compact) 71 | interpolate_paperclip_path(path) 72 | end 73 | 74 | def store_dir 75 | ":rails_root/public/system/:attachment/:id" 76 | end 77 | 78 | def paperclip_default_style 79 | :original 80 | end 81 | 82 | def paperclip_path 83 | end 84 | 85 | def paperclip_style 86 | version_name || paperclip_default_style 87 | end 88 | 89 | module ClassMethods 90 | def interpolate(sym, &block) 91 | mappings[sym] = block 92 | end 93 | end 94 | 95 | private 96 | def interpolate_paperclip_path(path) 97 | mappings.each_pair.inject(path) do |agg, pair| 98 | agg.gsub(":#{pair[0]}") { pair[1].call(self, self.paperclip_style).to_s } 99 | end 100 | end 101 | end # Paperclip 102 | end # Compatibility 103 | end # CarrierWave 104 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/file.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module Storage 3 | 4 | ## 5 | # File storage stores file to the Filesystem (surprising, no?). There's really not much 6 | # to it, it uses the store_dir defined on the uploader as the storage location. That's 7 | # pretty much it. 8 | # 9 | class File < Abstract 10 | 11 | ## 12 | # Move the file to the uploader's store path. 13 | # 14 | # By default, store!() uses copy_to(), which operates by copying the file 15 | # from the cache to the store, then deleting the file from the cache. 16 | # If move_to_store() is overriden to return true, then store!() uses move_to(), 17 | # which simply moves the file from cache to store. Useful for large files. 18 | # 19 | # === Parameters 20 | # 21 | # [file (CarrierWave::SanitizedFile)] the file to store 22 | # 23 | # === Returns 24 | # 25 | # [CarrierWave::SanitizedFile] a sanitized file 26 | # 27 | def store!(file) 28 | path = ::File.expand_path(uploader.store_path, uploader.root) 29 | if uploader.move_to_store 30 | file.move_to(path, uploader.permissions, uploader.directory_permissions) 31 | else 32 | file.copy_to(path, uploader.permissions, uploader.directory_permissions) 33 | end 34 | end 35 | 36 | ## 37 | # Retrieve the file from its store path 38 | # 39 | # === Parameters 40 | # 41 | # [identifier (String)] the filename of the file 42 | # 43 | # === Returns 44 | # 45 | # [CarrierWave::SanitizedFile] a sanitized file 46 | # 47 | def retrieve!(identifier) 48 | path = ::File.expand_path(uploader.store_path(identifier), uploader.root) 49 | CarrierWave::SanitizedFile.new(path) 50 | end 51 | 52 | ## 53 | # Stores given file to cache directory. 54 | # 55 | # === Parameters 56 | # 57 | # [new_file (File, IOString, Tempfile)] any kind of file object 58 | # 59 | # === Returns 60 | # 61 | # [CarrierWave::SanitizedFile] a sanitized file 62 | # 63 | def cache!(new_file) 64 | new_file.move_to(::File.expand_path(uploader.cache_path, uploader.root), uploader.permissions, uploader.directory_permissions, true) 65 | rescue Errno::EMLINK => e 66 | raise(e) if @cache_called 67 | @cache_called = true 68 | 69 | # NOTE: Remove cached files older than 10 minutes 70 | clean_cache!(600) 71 | 72 | cache!(new_file) 73 | end 74 | 75 | ## 76 | # Retrieves the file with the given cache_name from the cache. 77 | # 78 | # === Parameters 79 | # 80 | # [cache_name (String)] uniquely identifies a cache file 81 | # 82 | # === Raises 83 | # 84 | # [CarrierWave::InvalidParameter] if the cache_name is incorrectly formatted. 85 | # 86 | def retrieve_from_cache!(identifier) 87 | CarrierWave::SanitizedFile.new(::File.expand_path(uploader.cache_path(identifier), uploader.root)) 88 | end 89 | 90 | ## 91 | # Deletes a cache dir 92 | # 93 | def delete_dir!(path) 94 | if path 95 | begin 96 | Dir.rmdir(::File.expand_path(path, uploader.root)) 97 | rescue Errno::ENOENT 98 | # Ignore: path does not exist 99 | rescue Errno::ENOTDIR 100 | # Ignore: path is not a dir 101 | rescue Errno::ENOTEMPTY, Errno::EEXIST 102 | # Ignore: dir is not empty 103 | end 104 | end 105 | end 106 | 107 | def clean_cache!(seconds) 108 | Dir.glob(::File.expand_path(::File.join(uploader.cache_dir, '*'), CarrierWave.root)).each do |dir| 109 | # generate_cache_id returns key formated TIMEINT-PID-COUNTER-RND 110 | time = dir.scan(/(\d+)-\d+-\d+-\d+/).first.map(&:to_i) 111 | time = Time.at(*time) 112 | if time < (Time.now.utc - seconds) 113 | FileUtils.rm_rf(dir) 114 | end 115 | end 116 | end 117 | end # File 118 | end # Storage 119 | end # CarrierWave 120 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'pry' 5 | require 'tempfile' 6 | require 'time' 7 | require 'logger' 8 | 9 | require 'carrierwave' 10 | require 'timecop' 11 | require 'open-uri' 12 | require "webmock/rspec" 13 | require 'mini_magick' 14 | 15 | I18n.enforce_available_locales = false 16 | 17 | CARRIERWAVE_DIRECTORY = "carrierwave#{Time.now.to_i}" unless defined?(CARRIERWAVE_DIRECTORY) 18 | 19 | alias :running :lambda 20 | 21 | def file_path( *paths ) 22 | File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', *paths)) 23 | end 24 | 25 | def public_path( *paths ) 26 | File.expand_path(File.join(File.dirname(__FILE__), 'public', *paths)) 27 | end 28 | 29 | def tmp_path( *paths ) 30 | File.expand_path(File.join(File.dirname(__FILE__), 'tmp', *paths)) 31 | end 32 | 33 | CarrierWave.root = public_path 34 | I18n.load_path << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "carrierwave", "locale", 'en.yml')) 35 | 36 | module CarrierWave 37 | module Test 38 | module MockStorage 39 | def mock_storage(kind) 40 | storage = double("storage for #{kind} uploader") 41 | allow(storage).to receive(:setup!) 42 | storage 43 | end 44 | end 45 | 46 | module MockFiles 47 | def stub_tempfile(filename, mime_type=nil, fake_name=nil) 48 | raise "#{path} file does not exist" unless File.exist?(file_path(filename)) 49 | 50 | tempfile = Tempfile.new(filename) 51 | FileUtils.copy_file(file_path(filename), tempfile.path) 52 | allow(tempfile).to receive_messages(:original_filename => fake_name || filename, 53 | :content_type => mime_type) 54 | tempfile 55 | end 56 | 57 | alias_method :stub_merb_tempfile, :stub_tempfile 58 | 59 | def stub_stringio(filename, mime_type=nil, fake_name=nil) 60 | file = IO.read( file_path( filename ) ) if filename 61 | stringio = StringIO.new(file) 62 | allow(stringio).to receive_messages(:local_path => "", 63 | :original_filename => filename || fake_name, 64 | :content_type => mime_type) 65 | stringio 66 | end 67 | 68 | def stub_file(filename, mime_type=nil, fake_name=nil) 69 | File.open(file_path(filename)) 70 | end 71 | end 72 | 73 | module I18nHelpers 74 | def change_locale_and_store_translations(locale, translations, &block) 75 | current_locale = I18n.locale 76 | begin 77 | I18n.backend.store_translations locale, translations 78 | I18n.locale = locale 79 | yield 80 | ensure 81 | I18n.reload! 82 | I18n.locale = current_locale 83 | end 84 | end 85 | 86 | def change_and_enforece_available_locales(locale, available_locales, &block) 87 | current_available_locales = I18n.available_locales 88 | current_enforce_available_locales_value = I18n.enforce_available_locales 89 | current_locale = I18n.locale 90 | begin 91 | I18n.available_locales = [:nl] 92 | I18n.enforce_available_locales = true 93 | I18n.locale = :nl 94 | yield 95 | ensure 96 | I18n.available_locales = current_available_locales 97 | I18n.enforce_available_locales = current_enforce_available_locales_value 98 | I18n.locale = current_locale 99 | end 100 | end 101 | end 102 | 103 | module ManipulationHelpers 104 | def color_of_pixel(path, x, y) 105 | image = ::MiniMagick::Image.open(path) 106 | color = image.run_command("convert", "#{image.path}[1x1+#{x}+#{y}]", "-depth", "8", "txt:").split("\n")[1] 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 | config.include CarrierWave::Test::ManipulationHelpers 118 | if RUBY_ENGINE == 'jruby' 119 | config.filter_run_excluding :rmagick => true 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/uploader/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'carrierwave/storage/fog' 3 | 4 | describe CarrierWave do 5 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 6 | 7 | describe '.configure' do 8 | before do 9 | CarrierWave::Uploader::Base.add_config :test_config 10 | CarrierWave.configure { |config| config.test_config = "foo" } 11 | end 12 | 13 | it "proxies to Uploader configuration" do 14 | expect(CarrierWave::Uploader::Base.test_config).to eq('foo') 15 | end 16 | end 17 | end 18 | 19 | describe CarrierWave::Uploader::Base do 20 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 21 | 22 | describe '.configure' do 23 | before do 24 | uploader_class.tap do |uc| 25 | uc.add_config :foo_bar 26 | uc.configure { |config| config.foo_bar = "monkey" } 27 | end 28 | end 29 | 30 | it "sets a configuration parameter" do 31 | expect(uploader_class.foo_bar).to eq('monkey') 32 | end 33 | end 34 | 35 | describe ".storage" do 36 | let(:storage) { double('some kind of storage').as_null_object } 37 | 38 | it "sets the storage if an argument is given" do 39 | uploader_class.storage(storage) 40 | 41 | expect(uploader_class.storage).to storage 42 | end 43 | 44 | it "defaults to file" do 45 | expect(uploader_class.storage).to eq(CarrierWave::Storage::File) 46 | end 47 | 48 | it "sets the storage from the configured shortcuts if a symbol is given" do 49 | uploader_class.storage :file 50 | expect(uploader_class.storage).to eq(CarrierWave::Storage::File) 51 | end 52 | 53 | context "when inherited" do 54 | before { uploader_class.storage(:fog) } 55 | let(:subclass) { Class.new(uploader_class) } 56 | 57 | it "remembers the storage" do 58 | expect(subclass.storage).to eq(CarrierWave::Storage::Fog) 59 | end 60 | 61 | it "'s changeable" do 62 | expect(subclass.storage).to eq(CarrierWave::Storage::Fog) 63 | 64 | subclass.storage(:file) 65 | expect(subclass.storage).to eq(CarrierWave::Storage::File) 66 | end 67 | end 68 | 69 | it "raises UnknownStorageError when set unknown storage" do 70 | expect{ uploader_class.storage :unknown }.to raise_error(CarrierWave::UnknownStorageError, "Unknown storage: unknown") 71 | end 72 | end 73 | 74 | describe '.add_config' do 75 | before do 76 | uploader_class.add_config :foo_bar 77 | uploader_class.foo_bar = 'foo' 78 | end 79 | 80 | it "adds a class level accessor" do 81 | expect(uploader_class.foo_bar).to eq('foo') 82 | end 83 | 84 | it "adds an instance level accessor" do 85 | expect(uploader_class.new.foo_bar).to eq('foo') 86 | end 87 | 88 | it "adds a convenient in-class setter" do 89 | expect(uploader_class.foo_bar).to eq('foo') 90 | end 91 | 92 | ['foo', :foo, 45, ['foo', :bar]].each do |val| 93 | it "'s inheritable for a #{val.class}" do 94 | uploader_class.add_config :foo_bar 95 | child_class = Class.new(uploader_class) 96 | 97 | uploader_class.foo_bar = val 98 | expect(uploader_class.foo_bar).to eq(val) 99 | expect(child_class.foo_bar).to eq(val) 100 | 101 | child_class.foo_bar = "bar" 102 | expect(child_class.foo_bar).to eq("bar") 103 | 104 | expect(uploader_class.foo_bar).to eq(val) 105 | end 106 | end 107 | 108 | describe "assigning a proc to a config attribute" do 109 | before do 110 | uploader_class.tap do |uc| 111 | uc.add_config :hoobatz 112 | uc.hoobatz = this_proc 113 | end 114 | end 115 | 116 | context "when the proc accepts no arguments" do 117 | let(:this_proc) { proc { "a return value" } } 118 | 119 | it "calls the proc without arguments" do 120 | expect(uploader_class.new.hoobatz).to eq("a return value") 121 | end 122 | end 123 | 124 | context "when the proc accepts one argument" do 125 | let(:this_proc) { proc { |arg1| expect(arg1).to be_an_instance_of(uploader_class) } } 126 | 127 | it "calls the proc with an instance of the uploader" do 128 | uploader_class.new.hoobatz 129 | end 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/uploader/extension_blacklist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | subject { lambda { uploader.cache!(test_file) } } 5 | 6 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 7 | let(:uploader) { uploader_class.new } 8 | let(:cache_id) { '1369894322-345-1234-2255' } 9 | let(:test_file_name) { 'test.jpg' } 10 | let(:test_file) { File.open(file_path(test_file_name)) } 11 | 12 | after { FileUtils.rm_rf(public_path) } 13 | 14 | before { allow(CarrierWave).to receive(:generate_cache_id).and_return(cache_id) } 15 | 16 | describe '#cache!' do 17 | before { allow(uploader).to receive(:extension_blacklist).and_return(extension_blacklist) } 18 | 19 | context "when there are no blacklisted extensions" do 20 | let(:extension_blacklist) { nil } 21 | 22 | it "doesn't raise an integrity error" do 23 | is_expected.not_to raise_error 24 | end 25 | end 26 | 27 | context "when there is a blacklist" do 28 | context "when the blacklist is an array of values" do 29 | context "when the file extension matches a blacklisted extension" do 30 | let(:extension_blacklist) { %w(jpg gif png) } 31 | 32 | it "raises an integrity error" do 33 | is_expected.to raise_error(CarrierWave::IntegrityError) 34 | end 35 | end 36 | 37 | context "when the file extension doesn't match a blacklisted extension" do 38 | let(:extension_blacklist) { %w(txt doc xls) } 39 | 40 | it "doesn't raise an integrity error" do 41 | is_expected.to_not raise_error 42 | end 43 | end 44 | 45 | context "when the file extension has only the starting part of a blacklisted extension string" do 46 | let(:text_file_name) { 'bork.ttxt' } 47 | let(:extension_blacklist) { %w(txt) } 48 | 49 | it "doesn't raise an integrity error" do 50 | is_expected.to_not raise_error 51 | end 52 | end 53 | 54 | context "when the file extension has only the ending part of a blacklisted extension string" do 55 | let(:text_file_name) { 'bork.txtt' } 56 | let(:extension_blacklist) { %w(txt) } 57 | 58 | it "doesn't raise an integrity error" do 59 | is_expected.to_not raise_error 60 | end 61 | end 62 | 63 | context "when the file has a capitalized extension of a blacklisted extension" do 64 | let(:text_file_name) { 'case.JPG' } 65 | let(:extension_blacklist) { %w(jpg gif png) } 66 | 67 | it "raise an integrity error" do 68 | is_expected.to raise_error(CarrierWave::IntegrityError) 69 | end 70 | end 71 | 72 | context "when the file has an extension which matches a blacklisted capitalized extension" do 73 | let(:text_file_name) { 'test.jpg' } 74 | let(:extension_blacklist) { %w(JPG GIF PNG) } 75 | 76 | it "raise an integrity error" do 77 | is_expected.to raise_error(CarrierWave::IntegrityError) 78 | end 79 | end 80 | 81 | context "when the file has an extension which matches the blacklisted extension regular expression" do 82 | let(:text_file_name) { 'test.jpeg' } 83 | let(:extension_blacklist) { [/jpe?g/, 'gif', 'png'] } 84 | 85 | it "raise an integrity error" do 86 | is_expected.to raise_error(CarrierWave::IntegrityError) 87 | end 88 | end 89 | end 90 | 91 | context "when the blacklist is a single value" do 92 | context "when the file has an extension which is equal the blacklisted extension string" do 93 | let(:test_file_name) { 'test.jpeg' } 94 | let(:extension_blacklist) { 'jpeg' } 95 | 96 | it "raises an integrity error" do 97 | is_expected.to raise_error(CarrierWave::IntegrityError) 98 | end 99 | end 100 | 101 | context "when the file has a name which matches the blacklisted extension regular expression" do 102 | let(:text_file_name) { 'test.jpeg' } 103 | let(:extension_blacklist) { /jpe?g/ } 104 | 105 | it "raise an integrity error" do 106 | is_expected.to raise_error(CarrierWave::IntegrityError) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/uploader/extension_whitelist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | before do 5 | @uploader_class = Class.new(CarrierWave::Uploader::Base) 6 | @uploader = @uploader_class.new 7 | end 8 | 9 | after do 10 | FileUtils.rm_rf(public_path) 11 | end 12 | 13 | describe '#cache!' do 14 | before do 15 | allow(CarrierWave).to receive(:generate_cache_id).and_return('1369894322-345-1234-2255') 16 | end 17 | 18 | context "when there is no whitelist" do 19 | it "does not raise an integrity error" do 20 | allow(@uploader).to receive(:extension_whitelist).and_return(nil) 21 | 22 | expect(running { 23 | @uploader.cache!(File.open(file_path('test.jpg'))) 24 | }).not_to raise_error 25 | end 26 | end 27 | 28 | context "when there is a whitelist" do 29 | context "when the whitelist is an array of values" do 30 | it "does not raise an integrity error when the file has a whitelisted extension" do 31 | allow(@uploader).to receive(:extension_whitelist).and_return(%w(jpg gif png)) 32 | 33 | expect(running { 34 | @uploader.cache!(File.open(file_path('test.jpg'))) 35 | }).not_to raise_error 36 | end 37 | 38 | it "raises an integrity error if the file has not a whitelisted extension" do 39 | allow(@uploader).to receive(:extension_whitelist).and_return(%w(txt doc xls)) 40 | 41 | expect(running { 42 | @uploader.cache!(File.open(file_path('test.jpg'))) 43 | }).to raise_error(CarrierWave::IntegrityError) 44 | end 45 | 46 | it "raises an integrity error if the file has not a whitelisted extension, using start of string matcher" do 47 | allow(@uploader).to receive(:extension_whitelist).and_return(%w(txt)) 48 | 49 | expect(running { 50 | @uploader.cache!(File.open(file_path('bork.ttxt'))) 51 | }).to raise_error(CarrierWave::IntegrityError) 52 | end 53 | 54 | it "raises an integrity error if the file has not a whitelisted extension, using end of string matcher" do 55 | allow(@uploader).to receive(:extension_whitelist).and_return(%w(txt)) 56 | 57 | expect(running { 58 | @uploader.cache!(File.open(file_path('bork.txtt'))) 59 | }).to raise_error(CarrierWave::IntegrityError) 60 | end 61 | 62 | it "compares extensions in a case insensitive manner when capitalized extension provided" do 63 | allow(@uploader).to receive(:extension_whitelist).and_return(%w(jpg gif png)) 64 | 65 | expect(running { 66 | @uploader.cache!(File.open(file_path('case.JPG'))) 67 | }).not_to raise_error 68 | end 69 | 70 | it "compares extensions in a case insensitive manner when lowercase extension provided" do 71 | allow(@uploader).to receive(:extension_whitelist).and_return(%w(JPG GIF PNG)) 72 | 73 | expect(running { 74 | @uploader.cache!(File.open(file_path('test.jpg'))) 75 | }).not_to raise_error 76 | end 77 | 78 | it "accepts extensions as regular expressions" do 79 | allow(@uploader).to receive(:extension_whitelist).and_return([/jpe?g/, 'gif', 'png']) 80 | 81 | expect(running { 82 | @uploader.cache!(File.open(file_path('test.jpeg'))) 83 | }).not_to raise_error 84 | end 85 | 86 | it "accepts extensions as regular expressions in a case insensitive manner" do 87 | 88 | allow(@uploader).to receive(:extension_whitelist).and_return([/jpe?g/, 'gif', 'png']) 89 | expect(running { 90 | @uploader.cache!(File.open(file_path('case.JPG'))) 91 | }).not_to raise_error 92 | end 93 | end 94 | 95 | context "when the whitelist is a single value" do 96 | it "accepts a single extension string value" do 97 | allow(@uploader).to receive(:extension_whitelist).and_return('jpeg') 98 | 99 | expect { @uploader.cache!(File.open(file_path('test.jpg'))) }.to raise_error(CarrierWave::IntegrityError) 100 | end 101 | 102 | it "accepts a single extension regular expression value" do 103 | allow(@uploader).to receive(:extension_whitelist).and_return(/jpe?g/) 104 | 105 | expect { @uploader.cache!(File.open(file_path('bork.txt')))}.to raise_error(CarrierWave::IntegrityError) 106 | 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/compatibility/paperclip_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'carrierwave/orm/activerecord' 3 | 4 | module Rails; end unless defined?(Rails) 5 | 6 | describe CarrierWave::Compatibility::Paperclip do 7 | let(:uploader_class) do 8 | Class.new(CarrierWave::Uploader::Base) do 9 | include CarrierWave::Compatibility::Paperclip 10 | 11 | version :thumb 12 | version :list 13 | end 14 | end 15 | 16 | let(:model) { double('model') } 17 | 18 | let(:uploader) { uploader_class.new(model, :monkey) } 19 | 20 | before do 21 | allow(Rails).to receive(:root).and_return('/rails/root') 22 | allow(Rails).to receive(:env).and_return('test') 23 | allow(model).to receive(:id).and_return(23) 24 | allow(model).to receive(:ook).and_return('eek') 25 | allow(model).to receive(:money).and_return('monkey.png') 26 | end 27 | 28 | after { FileUtils.rm_rf(public_path) } 29 | 30 | describe '#store_path' do 31 | subject { uploader.store_path("monkey.png") } 32 | 33 | it "mimics paperclip default" do 34 | is_expected.to eq("/rails/root/public/system/monkeys/23/original/monkey.png") 35 | end 36 | 37 | it "interpolates the root path" do 38 | allow(uploader).to receive(:paperclip_path).and_return(":rails_root/foo/bar") 39 | is_expected.to eq(Rails.root + "/foo/bar") 40 | end 41 | 42 | it "interpolates the attachment" do 43 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:attachment/bar") 44 | is_expected.to eq("/foo/monkeys/bar") 45 | end 46 | 47 | it "interpolates the id" do 48 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:id/bar") 49 | is_expected.to eq("/foo/23/bar") 50 | end 51 | 52 | it "interpolates the id partition" do 53 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:id_partition/bar") 54 | is_expected.to eq("/foo/000/000/023/bar") 55 | end 56 | 57 | it "interpolates the basename" do 58 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:basename/bar") 59 | is_expected.to eq("/foo/monkey/bar") 60 | end 61 | 62 | it "interpolates the extension" do 63 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:extension/bar") 64 | is_expected.to eq("/foo/png/bar") 65 | end 66 | end 67 | 68 | describe '.interpolate' do 69 | subject { uploader.store_path("monkey.png") } 70 | 71 | before do 72 | uploader_class.interpolate :ook do |custom, style| 73 | custom.model.ook 74 | end 75 | 76 | uploader_class.interpolate :aak do |model, style| 77 | style 78 | end 79 | end 80 | 81 | it 'allows you to add custom interpolations' do 82 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:id/:ook") 83 | is_expected.to eq('/foo/23/eek') 84 | end 85 | 86 | it 'mimics paperclips arguments' do 87 | allow(uploader).to receive(:paperclip_path).and_return("/foo/:aak") 88 | is_expected.to eq('/foo/original') 89 | end 90 | 91 | context 'when multiple uploaders include the compatibility module' do 92 | let(:uploader) { uploader_class_other.new(model, :monkey) } 93 | let(:uploader_class_other) do 94 | Class.new(CarrierWave::Uploader::Base) do 95 | include CarrierWave::Compatibility::Paperclip 96 | 97 | version :thumb 98 | version :list 99 | end 100 | end 101 | 102 | before { allow(uploader).to receive(:paperclip_path).and_return("/foo/:id/:ook") } 103 | 104 | it "doesn't share custom interpolations" do 105 | is_expected.to eq('/foo/23/:ook') 106 | end 107 | end 108 | 109 | context 'when there are multiple versions' do 110 | let(:complex_uploader_class) do 111 | Class.new(CarrierWave::Uploader::Base) do 112 | include CarrierWave::Compatibility::Paperclip 113 | 114 | interpolate :ook do |model, style| 115 | 'eek' 116 | end 117 | 118 | version :thumb 119 | version :list 120 | 121 | def paperclip_path 122 | "#{public_path}/foo/:ook/:id/:style" 123 | end 124 | end 125 | end 126 | 127 | let(:uploader) { complex_uploader_class.new(model, :monkey) } 128 | let!(:file) { File.open(file_path('test.jpg')) } 129 | 130 | before { uploader.store!(file) } 131 | 132 | it 'interpolates for all versions correctly' do 133 | expect(uploader.thumb.path).to eq("#{public_path}/foo/eek/23/thumb") 134 | expect(uploader.list.path).to eq("#{public_path}/foo/eek/23/list") 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/carrierwave/mounter.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | 3 | # this is an internal class, used by CarrierWave::Mount so that 4 | # we don't pollute the model with a lot of methods. 5 | class Mounter #:nodoc: 6 | attr_reader :column, :record, :remote_urls, :integrity_error, 7 | :processing_error, :download_error 8 | attr_accessor :remove, :remote_request_headers 9 | 10 | def initialize(record, column, options={}) 11 | @record = record 12 | @column = column 13 | @options = record.class.uploader_options[column] 14 | end 15 | 16 | def uploader_class 17 | record.class.uploaders[column] 18 | end 19 | 20 | def blank_uploader 21 | uploader_class.new(record, column) 22 | end 23 | 24 | def identifiers 25 | uploaders.map(&:identifier) 26 | end 27 | 28 | def read_identifiers 29 | [record.read_uploader(serialization_column)].flatten.reject(&:blank?) 30 | end 31 | 32 | def uploaders 33 | @uploaders ||= read_identifiers.map do |identifier| 34 | uploader = blank_uploader 35 | uploader.retrieve_from_store!(identifier) if identifier.present? 36 | uploader 37 | end 38 | end 39 | 40 | def cache(new_files) 41 | return if not new_files or new_files == "" 42 | @uploaders = new_files.map do |new_file| 43 | uploader = blank_uploader 44 | uploader.cache!(new_file) 45 | uploader 46 | end 47 | 48 | @integrity_error = nil 49 | @processing_error = nil 50 | rescue CarrierWave::IntegrityError => e 51 | @integrity_error = e 52 | raise e unless option(:ignore_integrity_errors) 53 | rescue CarrierWave::ProcessingError => e 54 | @processing_error = e 55 | raise e unless option(:ignore_processing_errors) 56 | end 57 | 58 | def cache_names 59 | uploaders.map(&:cache_name).compact 60 | end 61 | 62 | def cache_names=(cache_names) 63 | return if not cache_names or cache_names == "" or uploaders.any?(&:cached?) 64 | @uploaders = cache_names.map do |cache_name| 65 | uploader = blank_uploader 66 | uploader.retrieve_from_cache!(cache_name) 67 | uploader 68 | end 69 | rescue CarrierWave::InvalidParameter 70 | end 71 | 72 | def remote_urls=(urls) 73 | return if not urls or urls == "" or urls.all?(&:blank?) 74 | 75 | @remote_urls = urls 76 | @download_error = nil 77 | @integrity_error = nil 78 | 79 | @uploaders = urls.zip(remote_request_headers || []).map do |url, header| 80 | uploader = blank_uploader 81 | uploader.download!(url, header || {}) 82 | uploader 83 | end 84 | 85 | rescue CarrierWave::DownloadError => e 86 | @download_error = e 87 | raise e unless option(:ignore_download_errors) 88 | rescue CarrierWave::ProcessingError => e 89 | @processing_error = e 90 | raise e unless option(:ignore_processing_errors) 91 | rescue CarrierWave::IntegrityError => e 92 | @integrity_error = e 93 | raise e unless option(:ignore_integrity_errors) 94 | end 95 | 96 | def store! 97 | if remove? 98 | remove! 99 | else 100 | uploaders.reject(&:blank?).each(&:store!) 101 | end 102 | end 103 | 104 | def urls(*args) 105 | uploaders.map { |u| u.url(*args) } 106 | end 107 | 108 | def blank? 109 | uploaders.none?(&:present?) 110 | end 111 | 112 | def remove? 113 | remove.present? && remove !~ /\A0|false$\z/ 114 | end 115 | 116 | def remove! 117 | uploaders.reject(&:blank?).each(&:remove!) 118 | @uploaders = [] 119 | end 120 | 121 | def serialization_column 122 | option(:mount_on) || column 123 | end 124 | 125 | def remove_previous(before=nil, after=nil) 126 | after ||= [] 127 | return unless before 128 | 129 | # both 'before' and 'after' can be string when 'mount_on' option is set 130 | before = before.reject(&:blank?).map do |value| 131 | if value.is_a?(String) 132 | uploader = blank_uploader 133 | uploader.retrieve_from_store!(value) 134 | uploader 135 | else 136 | value 137 | end 138 | end 139 | after_paths = after.reject(&:blank?).map do |value| 140 | if value.is_a?(String) 141 | uploader = blank_uploader 142 | uploader.retrieve_from_store!(value) 143 | uploader 144 | else 145 | value 146 | end.path 147 | end 148 | before.each do |uploader| 149 | if uploader.remove_previously_stored_files_after_update and not after_paths.include?(uploader.path) 150 | uploader.remove! 151 | end 152 | end 153 | end 154 | 155 | attr_accessor :uploader_options 156 | 157 | private 158 | 159 | def option(name) 160 | self.uploader_options ||= {} 161 | self.uploader_options[name] ||= record.class.uploader_option(column, name) 162 | end 163 | 164 | end # Mounter 165 | end # CarrierWave 166 | -------------------------------------------------------------------------------- /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/1369894322-345-1234-2255/bork.txt' 47 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/1369894322-345-1234-2255/thumb_bork.txt' 48 | Given the file 'fixtures/bork.txt' is cached file at 'public/uploads/tmp/1369894322-345-1234-2255/thumb_mini_bork.txt' 49 | Given the file 'fixtures/monkey.txt' is cached file at 'public/uploads/tmp/1369894322-345-1234-2255/thumb_micro_bork.txt' 50 | When I retrieve the cache name '1369894322-345-1234-2255/bork.txt' from the cache 51 | And I store the file 52 | Then there should be a file at 'public/uploads/bork.txt' 53 | Then there should be a file at 'public/uploads/thumb_bork.txt' 54 | Then there should be a file at 'public/uploads/thumb_mini_bork.txt' 55 | Then there should be a file at 'public/uploads/thumb_micro_bork.txt' 56 | And the file at 'public/uploads/bork.txt' should be identical to the file at 'fixtures/bork.txt' 57 | And the file at 'public/uploads/thumb_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 58 | And the file at 'public/uploads/thumb_mini_bork.txt' should be identical to the file at 'fixtures/bork.txt' 59 | And the file at 'public/uploads/thumb_micro_bork.txt' should be identical to the file at 'fixtures/monkey.txt' 60 | 61 | Scenario: retrieving a file from store 62 | Given the file 'fixtures/bork.txt' is stored at 'public/uploads/bork.txt' 63 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_bork.txt' 64 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_mini_bork.txt' 65 | Given the file 'fixtures/monkey.txt' is stored at 'public/uploads/thumb_micro_bork.txt' 66 | When I retrieve the file 'bork.txt' from the store 67 | Then the uploader should have the url '/uploads/bork.txt' 68 | And the uploader's version 'thumb' should have the url '/uploads/thumb_bork.txt' 69 | And the uploader's nested version 'mini' nested in 'thumb' should have the url '/uploads/thumb_mini_bork.txt' 70 | And the uploader's nested version 'micro' nested in 'thumb' should have the url '/uploads/thumb_micro_bork.txt' 71 | -------------------------------------------------------------------------------- /spec/uploader/processing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | 7 | after { FileUtils.rm_rf(public_path) } 8 | 9 | describe '.process' do 10 | context "when a symbol is given" do 11 | before { uploader_class.process(process_param) } 12 | after { uploader.process! } 13 | 14 | let(:process_param) { :sepiatone } 15 | 16 | it "adds a single processor" do 17 | expect(uploader).to receive(:sepiatone) 18 | end 19 | end 20 | 21 | context "when an array of symbols is given" do 22 | before { uploader_class.process(*process_param) } 23 | after { uploader.process! } 24 | 25 | let(:process_param) { [:sepiatone, :desaturate, :invert] } 26 | 27 | it "adds multiple processors" do 28 | expect(uploader).to receive(:sepiatone) 29 | expect(uploader).to receive(:desaturate) 30 | expect(uploader).to receive(:invert) 31 | end 32 | end 33 | 34 | it "adds a single processor with an argument when a hash is given" do 35 | uploader_class.process :format => 'png' 36 | expect(uploader).to receive(:format).with('png') 37 | uploader.process! 38 | end 39 | 40 | it "adds a single processor with several argument when a hash is given" do 41 | uploader_class.process :resize => [200, 300] 42 | expect(uploader).to receive(:resize).with(200, 300) 43 | uploader.process! 44 | end 45 | 46 | it "adds multiple processors when an hash with multiple keys is given" do 47 | uploader_class.process :resize => [200, 300], :format => 'png' 48 | expect(uploader).to receive(:resize).with(200, 300) 49 | expect(uploader).to receive(:format).with('png') 50 | uploader.process! 51 | end 52 | 53 | it "calls the processor if the condition method returns true" do 54 | uploader_class.process :resize => [200, 300], :if => :true? 55 | uploader_class.process :fancy, :if => :true? 56 | expect(uploader).to receive(:true?).with("test.jpg").twice.and_return(true) 57 | expect(uploader).to receive(:resize).with(200, 300) 58 | expect(uploader).to receive(:fancy) 59 | uploader.process!("test.jpg") 60 | end 61 | 62 | it "doesn't call the processor if the condition method returns false" do 63 | uploader_class.process :resize => [200, 300], :if => :false? 64 | uploader_class.process :fancy, :if => :false? 65 | expect(uploader).to receive(:false?).with("test.jpg").twice.and_return(false) 66 | expect(uploader).not_to receive(:resize) 67 | expect(uploader).not_to receive(:fancy) 68 | uploader.process!("test.jpg") 69 | end 70 | 71 | it "calls the processor if the condition block returns true" do 72 | uploader_class.process :resize => [200, 300], :if => lambda{|record, args| record.true?(args[:file])} 73 | uploader_class.process :fancy, :if => :true? 74 | expect(uploader).to receive(:true?).with("test.jpg").twice.and_return(true) 75 | expect(uploader).to receive(:resize).with(200, 300) 76 | expect(uploader).to receive(:fancy) 77 | uploader.process!("test.jpg") 78 | end 79 | 80 | it "doesn't call the processor if the condition block returns false" do 81 | uploader_class.process :resize => [200, 300], :if => lambda{|record, args| record.false?(args[:file])} 82 | uploader_class.process :fancy, :if => :false? 83 | expect(uploader).to receive(:false?).with("test.jpg").twice.and_return(false) 84 | expect(uploader).not_to receive(:resize) 85 | expect(uploader).not_to receive(:fancy) 86 | uploader.process!("test.jpg") 87 | end 88 | 89 | context "when using RMagick", :rmagick => true do 90 | before do 91 | def uploader.cover 92 | manipulate! { |frame, index| frame if index.zero? } 93 | end 94 | 95 | uploader_class.send :include, CarrierWave::RMagick 96 | end 97 | 98 | after { uploader.instance_eval { undef cover } } 99 | 100 | context "with a multi-page PDF" do 101 | before { uploader.cache! File.open(file_path("multi_page.pdf")) } 102 | 103 | it "successfully processes" do 104 | uploader_class.process :convert => 'jpg' 105 | uploader.process! 106 | end 107 | 108 | it "supports page specific transformations" do 109 | uploader_class.process :cover 110 | uploader.process! 111 | end 112 | end 113 | 114 | context "with a simple image" do 115 | before { uploader.cache! File.open(file_path("portrait.jpg")) } 116 | 117 | it "allows page specific transformations" do 118 | uploader_class.process :cover 119 | uploader.process! 120 | end 121 | end 122 | end 123 | 124 | context "with 'enable_processing' set to false" do 125 | before { uploader_class.enable_processing = false } 126 | 127 | it "doesn't do any processing" do 128 | uploader_class.process :sepiatone, :desaturate, :invert 129 | expect(uploader).not_to receive(:sepiatone) 130 | expect(uploader).not_to receive(:desaturate) 131 | expect(uploader).not_to receive(:invert) 132 | uploader.process! 133 | end 134 | end 135 | end 136 | 137 | describe '#cache!' do 138 | before do 139 | allow(CarrierWave).to receive(:generate_cache_id).and_return('1369894322-345-1234-2255') 140 | end 141 | 142 | it "triggers a process!" do 143 | expect(uploader).to receive(:process!) 144 | uploader.cache!(File.open(file_path('test.jpg'))) 145 | end 146 | end 147 | 148 | describe '#recreate_versions!' do 149 | before do 150 | allow(CarrierWave).to receive(:generate_cache_id).and_return('1369894322-345-1234-2255') 151 | end 152 | 153 | it "triggers a process!" do 154 | uploader.store!(File.open(file_path('test.jpg'))) 155 | expect(uploader).to receive(:process!) 156 | uploader.recreate_versions! 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/carrierwave/uploader/cache.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | 3 | class FormNotMultipart < UploadError 4 | def message 5 | "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." 6 | end 7 | end 8 | 9 | class CacheCounter 10 | @@counter = 0 11 | 12 | def self.increment 13 | @@counter += 1 14 | end 15 | end 16 | 17 | ## 18 | # Generates a unique cache id for use in the caching system 19 | # 20 | # === Returns 21 | # 22 | # [String] a cache id in the format TIMEINT-PID-COUNTER-RND 23 | # 24 | def self.generate_cache_id 25 | [Time.now.utc.to_i, 26 | Process.pid, 27 | '%04d' % (CarrierWave::CacheCounter.increment % 1000), 28 | '%04d' % rand(9999) 29 | ].map(&:to_s).join('-') 30 | end 31 | 32 | module Uploader 33 | module Cache 34 | extend ActiveSupport::Concern 35 | 36 | include CarrierWave::Uploader::Callbacks 37 | include CarrierWave::Uploader::Configuration 38 | 39 | module ClassMethods 40 | 41 | ## 42 | # Removes cached files which are older than one day. You could call this method 43 | # from a rake task to clean out old cached files. 44 | # 45 | # You can call this method directly on the module like this: 46 | # 47 | # CarrierWave.clean_cached_files! 48 | # 49 | # === Note 50 | # 51 | # This only works as long as you haven't done anything funky with your cache_dir. 52 | # It's recommended that you keep cache files in one place only. 53 | # 54 | def clean_cached_files!(seconds=60*60*24) 55 | cache_storage.new(CarrierWave::Uploader::Base.new).clean_cache!(seconds) 56 | end 57 | end 58 | 59 | ## 60 | # Returns true if the uploader has been cached 61 | # 62 | # === Returns 63 | # 64 | # [Bool] whether the current file is cached 65 | # 66 | def cached? 67 | @cache_id 68 | end 69 | 70 | ## 71 | # Caches the remotely stored file 72 | # 73 | # This is useful when about to process images. Most processing solutions 74 | # require the file to be stored on the local filesystem. 75 | # 76 | def cache_stored_file! 77 | cache! 78 | end 79 | 80 | def sanitized_file 81 | _content = file.read 82 | if _content.is_a?(File) # could be if storage is Fog 83 | sanitized = CarrierWave::Storage::Fog.new(self).retrieve!(File.basename(_content.path)) 84 | else 85 | sanitized = SanitizedFile.new :tempfile => StringIO.new(_content), 86 | :filename => File.basename(path), :content_type => file.content_type 87 | end 88 | sanitized 89 | end 90 | 91 | ## 92 | # Returns a String which uniquely identifies the currently cached file for later retrieval 93 | # 94 | # === Returns 95 | # 96 | # [String] a cache name, in the format TIMEINT-PID-COUNTER-RND/filename.txt 97 | # 98 | def cache_name 99 | File.join(cache_id, full_original_filename) if cache_id and original_filename 100 | end 101 | 102 | ## 103 | # Caches the given file. Calls process! to trigger any process callbacks. 104 | # 105 | # By default, cache!() uses copy_to(), which operates by copying the file 106 | # to the cache, then deleting the original file. If move_to_cache() is 107 | # overriden to return true, then cache!() uses move_to(), which simply 108 | # moves the file to the cache. Useful for large files. 109 | # 110 | # === Parameters 111 | # 112 | # [new_file (File, IOString, Tempfile)] any kind of file object 113 | # 114 | # === Raises 115 | # 116 | # [CarrierWave::FormNotMultipart] if the assigned parameter is a string 117 | # 118 | def cache!(new_file = sanitized_file) 119 | new_file = CarrierWave::SanitizedFile.new(new_file) 120 | return if new_file.empty? 121 | 122 | raise CarrierWave::FormNotMultipart if new_file.is_path? && ensure_multipart_form 123 | 124 | self.cache_id = CarrierWave.generate_cache_id unless cache_id 125 | 126 | @filename = new_file.filename 127 | self.original_filename = new_file.filename 128 | 129 | begin 130 | # first, create a workfile on which we perform processings 131 | if move_to_cache 132 | @file = new_file.move_to(File.expand_path(workfile_path, root), permissions, directory_permissions) 133 | else 134 | @file = new_file.copy_to(File.expand_path(workfile_path, root), permissions, directory_permissions) 135 | end 136 | 137 | with_callbacks(:cache, @file) do 138 | @file = cache_storage.cache!(@file) 139 | end 140 | ensure 141 | FileUtils.rm_rf(workfile_path('')) 142 | end 143 | end 144 | 145 | ## 146 | # Retrieves the file with the given cache_name from the cache. 147 | # 148 | # === Parameters 149 | # 150 | # [cache_name (String)] uniquely identifies a cache file 151 | # 152 | # === Raises 153 | # 154 | # [CarrierWave::InvalidParameter] if the cache_name is incorrectly formatted. 155 | # 156 | def retrieve_from_cache!(cache_name) 157 | with_callbacks(:retrieve_from_cache, cache_name) do 158 | self.cache_id, self.original_filename = cache_name.to_s.split('/', 2) 159 | @filename = original_filename 160 | @file = cache_storage.retrieve_from_cache!(full_filename(original_filename)) 161 | end 162 | end 163 | 164 | ## 165 | # Calculates the path where the cache file should be stored. 166 | # 167 | # === Parameters 168 | # 169 | # [for_file (String)] name of the file 170 | # 171 | # === Returns 172 | # 173 | # [String] the cache path 174 | # 175 | def cache_path(for_file=full_filename(original_filename)) 176 | File.join(*[cache_dir, @cache_id, for_file].compact) 177 | end 178 | 179 | private 180 | 181 | def workfile_path(for_file=original_filename) 182 | File.join(CarrierWave.tmp_path, @cache_id, version_name.to_s, for_file) 183 | end 184 | 185 | attr_reader :cache_id, :original_filename 186 | 187 | # We can override the full_original_filename method in other modules 188 | alias_method :full_original_filename, :original_filename 189 | 190 | def cache_id=(cache_id) 191 | # Earlier version used 3 part cache_id. Thus we should allow for 192 | # the cache_id to have both 3 part and 4 part formats. 193 | raise CarrierWave::InvalidParameter, "invalid cache id" unless cache_id =~ /\A(-)?[\d]+\-[\d]+(\-[\d]{4})?\-[\d]{4}\z/ 194 | @cache_id = cache_id 195 | end 196 | 197 | def original_filename=(filename) 198 | raise CarrierWave::InvalidParameter, "invalid filename" if filename =~ CarrierWave::SanitizedFile.sanitize_regexp 199 | @original_filename = filename 200 | end 201 | 202 | def cache_storage 203 | @cache_storage ||= self.class.cache_storage.new(self) 204 | end 205 | end # Cache 206 | end # Uploader 207 | end # CarrierWave 208 | -------------------------------------------------------------------------------- /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, :_cache_storage, :instance_writer => false 9 | 10 | add_config :root 11 | add_config :base_path 12 | add_config :asset_host 13 | add_config :permissions 14 | add_config :directory_permissions 15 | add_config :storage_engines 16 | add_config :store_dir 17 | add_config :cache_dir 18 | add_config :enable_processing 19 | add_config :ensure_multipart_form 20 | add_config :delete_tmp_file_after_storage 21 | add_config :move_to_cache 22 | add_config :move_to_store 23 | add_config :remove_previously_stored_files_after_update 24 | 25 | # fog 26 | add_config :fog_provider 27 | add_config :fog_attributes 28 | add_config :fog_credentials 29 | add_config :fog_directory 30 | add_config :fog_public 31 | add_config :fog_authenticated_url_expiration 32 | add_config :fog_use_ssl_for_aws 33 | add_config :fog_aws_accelerate 34 | 35 | # Mounting 36 | add_config :ignore_integrity_errors 37 | add_config :ignore_processing_errors 38 | add_config :ignore_download_errors 39 | add_config :validate_integrity 40 | add_config :validate_processing 41 | add_config :validate_download 42 | add_config :mount_on 43 | add_config :cache_only 44 | 45 | # set default values 46 | reset_config 47 | end 48 | 49 | module ClassMethods 50 | 51 | ## 52 | # Sets the storage engine to be used when storing files with this uploader. 53 | # Can be any class that implements a #store!(CarrierWave::SanitizedFile) and a #retrieve! 54 | # method. See lib/carrierwave/storage/file.rb for an example. Storage engines should 55 | # be added to CarrierWave::Uploader::Base.storage_engines so they can be referred 56 | # to by a symbol, which should be more convenient 57 | # 58 | # If no argument is given, it will simply return the currently used storage engine. 59 | # 60 | # === Parameters 61 | # 62 | # [storage (Symbol, Class)] The storage engine to use for this uploader 63 | # 64 | # === Returns 65 | # 66 | # [Class] the storage engine to be used with this uploader 67 | # 68 | # === Examples 69 | # 70 | # storage :file 71 | # storage CarrierWave::Storage::File 72 | # storage MyCustomStorageEngine 73 | # 74 | def storage(storage = nil) 75 | case storage 76 | when Symbol 77 | if storage_engine = storage_engines[storage] 78 | self._storage = eval storage_engine 79 | else 80 | raise CarrierWave::UnknownStorageError, "Unknown storage: #{storage}" 81 | end 82 | when nil 83 | storage 84 | else 85 | self._storage = storage 86 | end 87 | _storage 88 | end 89 | alias_method :storage=, :storage 90 | 91 | ## 92 | # Sets the cache storage engine to be used when storing cache files with this uploader. 93 | # Same as .storage except for required methods being #cache!(CarrierWave::SanitizedFile), 94 | # #retrieve_from_cache! and #delete_dir!. 95 | # 96 | # === Parameters 97 | # 98 | # [storage (Symbol, Class)] The cache storage engine to use for this uploader 99 | # 100 | # === Returns 101 | # 102 | # [Class] the cache storage engine to be used with this uploader 103 | # 104 | # === Examples 105 | # 106 | # cache_storage :file 107 | # cache_storage CarrierWave::Storage::File 108 | # cache_storage MyCustomStorageEngine 109 | # 110 | def cache_storage(storage = nil) 111 | if storage 112 | self._cache_storage = storage.is_a?(Symbol) ? eval(storage_engines[storage]) : storage 113 | end 114 | _cache_storage 115 | end 116 | alias_method :cache_storage=, :cache_storage 117 | 118 | def add_config(name) 119 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 120 | def self.eager_load_fog(fog_credentials) 121 | # see #1198. This will hopefully no longer be necessary after fog 2.0 122 | require self.fog_provider 123 | require 'carrierwave/storage/fog' 124 | Fog::Storage.new(fog_credentials) if fog_credentials.present? 125 | end 126 | 127 | def self.#{name}(value=nil) 128 | @#{name} = value if value 129 | eager_load_fog(value) if value && '#{name}' == 'fog_credentials' 130 | return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name}) 131 | name = superclass.#{name} 132 | return nil if name.nil? && !instance_variable_defined?("@#{name}") 133 | @#{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 134 | end 135 | 136 | def self.#{name}=(value) 137 | eager_load_fog(value) if '#{name}' == 'fog_credentials' && value.present? 138 | @#{name} = value 139 | end 140 | 141 | def #{name}=(value) 142 | self.class.eager_load_fog(value) if '#{name}' == 'fog_credentials' && value.present? 143 | @#{name} = value 144 | end 145 | 146 | def #{name} 147 | value = @#{name} if instance_variable_defined?(:@#{name}) 148 | value = self.class.#{name} unless instance_variable_defined?(:@#{name}) 149 | if value.instance_of?(Proc) 150 | value.arity >= 1 ? value.call(self) : value.call 151 | else 152 | value 153 | end 154 | end 155 | RUBY 156 | end 157 | 158 | def configure 159 | yield self 160 | end 161 | 162 | ## 163 | # sets configuration back to default 164 | # 165 | def reset_config 166 | configure do |config| 167 | config.permissions = 0644 168 | config.directory_permissions = 0755 169 | config.storage_engines = { 170 | :file => "CarrierWave::Storage::File", 171 | :fog => "CarrierWave::Storage::Fog" 172 | } 173 | config.storage = :file 174 | config.cache_storage = :file 175 | config.fog_provider = 'fog' 176 | config.fog_attributes = {} 177 | config.fog_credentials = {} 178 | config.fog_public = true 179 | config.fog_authenticated_url_expiration = 600 180 | config.fog_use_ssl_for_aws = true 181 | config.fog_aws_accelerate = false 182 | config.store_dir = 'uploads' 183 | config.cache_dir = 'uploads/tmp' 184 | config.delete_tmp_file_after_storage = true 185 | config.move_to_cache = false 186 | config.move_to_store = false 187 | config.remove_previously_stored_files_after_update = true 188 | config.ignore_integrity_errors = true 189 | config.ignore_processing_errors = true 190 | config.ignore_download_errors = true 191 | config.validate_integrity = true 192 | config.validate_processing = true 193 | config.validate_download = true 194 | config.root = lambda { CarrierWave.root } 195 | config.base_path = CarrierWave.base_path 196 | config.enable_processing = true 197 | config.ensure_multipart_form = true 198 | end 199 | end 200 | end 201 | 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/CollectionMethods: 2 | Description: Preferred collection methods. 3 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size 4 | Enabled: true 5 | PreferredMethods: 6 | collect: map 7 | collect!: map! 8 | find: detect 9 | find_all: select 10 | reduce: inject 11 | 12 | Style/DotPosition: 13 | Description: Checks the position of the dot in multi-line method calls. 14 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains 15 | Enabled: true 16 | EnforcedStyle: trailing 17 | SupportedStyles: 18 | - leading 19 | - trailing 20 | 21 | Style/PredicateName: 22 | Description: Check the names of predicate methods. 23 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark 24 | Enabled: true 25 | NamePrefix: 26 | - is_ 27 | - has_ 28 | - have_ 29 | NamePrefixBlacklist: 30 | - is_ 31 | Exclude: 32 | - spec/**/* 33 | 34 | Style/SingleLineMethods: 35 | Description: Avoid single-line methods. 36 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-single-line-methods 37 | Enabled: true 38 | AllowIfMethodIsEmpty: true 39 | 40 | Style/StringLiterals: 41 | Description: Checks if uses of quotes match the configured preference. 42 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals 43 | Enabled: true 44 | EnforcedStyle: double_quotes 45 | SupportedStyles: 46 | - single_quotes 47 | - double_quotes 48 | 49 | Style/StringLiteralsInInterpolation: 50 | Description: Checks if uses of quotes inside expressions in interpolated strings 51 | match the configured preference. 52 | Enabled: true 53 | EnforcedStyle: single_quotes 54 | SupportedStyles: 55 | - single_quotes 56 | - double_quotes 57 | 58 | Metrics/AbcSize: 59 | Description: A calculated magnitude based on number of assignments, branches, and 60 | conditions. 61 | Enabled: false 62 | Max: 15 63 | 64 | Metrics/ClassLength: 65 | Description: Avoid classes longer than 100 lines of code. 66 | Enabled: false 67 | CountComments: false 68 | Max: 100 69 | 70 | Metrics/ModuleLength: 71 | CountComments: false 72 | Max: 100 73 | Description: Avoid modules longer than 100 lines of code. 74 | Enabled: false 75 | 76 | Metrics/CyclomaticComplexity: 77 | Description: A complexity metric that is strongly correlated to the number of test 78 | cases needed to validate a method. 79 | Enabled: false 80 | Max: 6 81 | 82 | Metrics/MethodLength: 83 | Description: Avoid methods longer than 10 lines of code. 84 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods 85 | Enabled: false 86 | CountComments: false 87 | Max: 10 88 | 89 | Metrics/ParameterLists: 90 | Description: Avoid parameter lists longer than three or four parameters. 91 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#too-many-params 92 | Enabled: false 93 | Max: 5 94 | CountKeywordArgs: true 95 | 96 | Metrics/PerceivedComplexity: 97 | Description: A complexity metric geared towards measuring complexity for a human 98 | reader. 99 | Enabled: false 100 | Max: 7 101 | 102 | Lint/AssignmentInCondition: 103 | Description: Don't use assignment in conditions. 104 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition 105 | Enabled: false 106 | AllowSafeAssignment: true 107 | 108 | Lint/EachWithObjectArgument: 109 | Description: Check for immutable argument given to each_with_object. 110 | Enabled: true 111 | 112 | Lint/HandleExceptions: 113 | Description: Don't suppress exception. 114 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions 115 | Enabled: false 116 | 117 | Lint/LiteralInCondition: 118 | Description: Checks of literals used in conditions. 119 | Enabled: false 120 | 121 | Lint/LiteralInInterpolation: 122 | Description: Checks for literals used in interpolation. 123 | Enabled: false 124 | 125 | Style/InlineComment: 126 | Description: Avoid inline comments. 127 | Enabled: false 128 | 129 | Style/AccessorMethodName: 130 | Description: Check the naming of accessor methods for get_/set_. 131 | Enabled: false 132 | 133 | Style/Alias: 134 | Description: Use alias_method instead of alias. 135 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#alias-method 136 | Enabled: false 137 | 138 | Style/Documentation: 139 | Description: Document classes and non-namespace modules. 140 | Enabled: false 141 | 142 | Style/DoubleNegation: 143 | Description: Checks for uses of double negation (!!). 144 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-bang-bang 145 | Enabled: false 146 | 147 | Style/EachWithObject: 148 | Description: Prefer `each_with_object` over `inject` or `reduce`. 149 | Enabled: false 150 | 151 | Style/EmptyLiteral: 152 | Description: Prefer literals to Array.new/Hash.new/String.new. 153 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#literal-array-hash 154 | Enabled: false 155 | 156 | Style/ModuleFunction: 157 | Description: Checks for usage of `extend self` in modules. 158 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#module-function 159 | Enabled: false 160 | 161 | Style/OneLineConditional: 162 | Description: Favor the ternary operator(?:) over if/then/else/end constructs. 163 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#ternary-operator 164 | Enabled: false 165 | 166 | Style/PerlBackrefs: 167 | Description: Avoid Perl-style regex back references. 168 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers 169 | Enabled: false 170 | 171 | Style/Send: 172 | Description: Prefer `Object#__send__` or `Object#public_send` to `send`, as `send` 173 | may overlap with existing methods. 174 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#prefer-public-send 175 | Enabled: false 176 | 177 | Style/SpecialGlobalVars: 178 | Description: Avoid Perl-style global variables. 179 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms 180 | Enabled: false 181 | 182 | Style/VariableInterpolation: 183 | Description: Don't interpolate global, instance and class variables directly in 184 | strings. 185 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#curlies-interpolate 186 | Enabled: false 187 | 188 | Style/WhenThen: 189 | Description: Use when x then ... for one-line cases. 190 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#one-line-cases 191 | Enabled: false 192 | 193 | Style/RaiseArgs: 194 | Description: Checks the arguments passed to raise/fail. 195 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages 196 | Enabled: false 197 | EnforcedStyle: exploded 198 | SupportedStyles: 199 | - compact 200 | - exploded 201 | 202 | Style/SignalException: 203 | Description: Checks for proper usage of fail and raise. 204 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#fail-method 205 | Enabled: false 206 | EnforcedStyle: semantic 207 | SupportedStyles: 208 | - only_raise 209 | - only_fail 210 | - semantic 211 | 212 | Style/SingleLineBlockParams: 213 | Description: Enforces the names of some block params. 214 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#reduce-blocks 215 | Enabled: false 216 | Methods: 217 | - reduce: 218 | - a 219 | - e 220 | - inject: 221 | - a 222 | - e 223 | 224 | Style/GuardClause: 225 | Description: Check for conditionals that can be replaced with guard clauses 226 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals 227 | Enabled: false 228 | MinBodyLength: 1 229 | 230 | Style/IfUnlessModifier: 231 | Description: Favor modifier if/unless usage when you have a single-line body. 232 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier 233 | Enabled: false 234 | MaxLineLength: 80 235 | 236 | Style/OptionHash: 237 | Description: Don't use option hashes when you can use keyword arguments. 238 | Enabled: false 239 | 240 | Style/PercentLiteralDelimiters: 241 | Description: Use `%`-literal delimiters consistently 242 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-literal-braces 243 | Enabled: false 244 | PreferredDelimiters: 245 | "%": "()" 246 | "%i": "()" 247 | "%q": "()" 248 | "%Q": "()" 249 | "%r": "{}" 250 | "%s": "()" 251 | "%w": "()" 252 | "%W": "()" 253 | "%x": "()" 254 | 255 | Style/TrailingComma: 256 | Description: Checks for trailing comma in parameter lists and literals. 257 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas 258 | Enabled: false 259 | EnforcedStyleForMultiline: no_comma 260 | SupportedStyles: 261 | - comma 262 | - no_comma 263 | -------------------------------------------------------------------------------- /spec/uploader/url_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_support/json' 3 | 4 | describe CarrierWave::Uploader do 5 | 6 | let(:uploader) { MyCoolUploader.new } 7 | 8 | before { class MyCoolUploader < CarrierWave::Uploader::Base; end } 9 | 10 | after do 11 | FileUtils.rm_rf(public_path) 12 | Object.send(:remove_const, "MyCoolUploader") if defined?(::MyCoolUploader) 13 | end 14 | 15 | let(:cache_id) { '1369894322-345-1234-2255' } 16 | let(:test_file) { File.open(file_path(test_file_name)) } 17 | let(:test_file_name) { 'test.jpg' } 18 | 19 | before { allow(CarrierWave).to receive(:generate_cache_id).and_return(cache_id) } 20 | 21 | describe '#url' do 22 | subject(:url) { uploader.url } 23 | 24 | it { is_expected.to be_nil } 25 | 26 | it "doesn't raise exception when hash specified as argument" do 27 | expect { uploader.url({}) }.not_to raise_error 28 | end 29 | 30 | it "encodes the path of a file without an asset host" do 31 | uploader.cache!(File.open(file_path('test+.jpg'))) 32 | is_expected.to eq("/uploads/tmp/#{cache_id}/test%2B.jpg") 33 | end 34 | 35 | context "with a cached file" do 36 | before { uploader.cache!(test_file) } 37 | 38 | it "gets the directory relative to public, prepending a slash" do 39 | is_expected.to eq("/uploads/tmp/#{cache_id}/#{test_file_name}") 40 | end 41 | 42 | describe "File#url" do 43 | before do 44 | allow(uploader.file).to receive(:url).and_return(file_url) 45 | end 46 | 47 | context "when present" do 48 | let(:file_url) { 'http://www.example.com/someurl.jpg' } 49 | 50 | it { is_expected.to eq(file_url) } 51 | end 52 | 53 | context "when blank" do 54 | let(:file_url) { '' } 55 | 56 | it "returns the relative path" do 57 | is_expected.to eq("/uploads/tmp/#{cache_id}/#{test_file_name}") 58 | end 59 | end 60 | end 61 | end 62 | 63 | context "when File#url method doesn't get params" do 64 | before do 65 | module StorageX 66 | class File 67 | def url 68 | true 69 | end 70 | end 71 | end 72 | 73 | allow(uploader).to receive(:file).and_return(StorageX::File.new) 74 | end 75 | 76 | it "raises ArgumentError" do 77 | expect { uploader.url }.not_to raise_error 78 | end 79 | end 80 | 81 | describe "(:thumb)" do 82 | subject { uploader.url(:thumb) } 83 | 84 | it "raises ArgumentError when version doesn't exist" do 85 | expect { uploader.url(:thumb) }.to raise_error(ArgumentError) 86 | end 87 | 88 | context "when version is specified" do 89 | before do 90 | MyCoolUploader.version(:thumb) 91 | uploader.cache!(test_file) 92 | end 93 | 94 | it "doesn't raise ArgumentError when versions version exists" do 95 | expect { uploader.url(:thumb) }.not_to raise_error 96 | end 97 | 98 | it "gets the directory relative to public for a specific version" do 99 | is_expected.to eq("/uploads/tmp/#{cache_id}/thumb_#{test_file_name}") 100 | end 101 | 102 | describe "asset_host" do 103 | before { uploader.class.configure { |config| config.asset_host = asset_host } } 104 | 105 | context "when set as a string" do 106 | let(:asset_host) { "http://foo.bar" } 107 | 108 | it "prepends the string" do 109 | is_expected.to eq("#{asset_host}/uploads/tmp/#{cache_id}/thumb_#{test_file_name}") 110 | end 111 | 112 | describe "encoding" do 113 | let(:test_file_name) { 'test+.jpg' } 114 | 115 | it "encodes the path of a file" do 116 | is_expected.to eq("#{asset_host}/uploads/tmp/#{cache_id}/thumb_test%2B.jpg") 117 | end 118 | 119 | it "double-encodes the path of an available File#url" do 120 | url = 'http://www.example.com/directory%2Bname/another%2Bdirectory/some%2Burl.jpg' 121 | allow(uploader.file).to receive(:url).and_return(url) 122 | 123 | expect(uploader.url).to eq(url) 124 | end 125 | end 126 | end 127 | 128 | context "when set as a proc" do 129 | let(:asset_host) { proc { "http://foo.bar" } } 130 | 131 | it "prepends the result of proc" do 132 | is_expected.to eq("#{asset_host.call}/uploads/tmp/#{cache_id}/thumb_#{test_file_name}") 133 | end 134 | 135 | describe "encoding" do 136 | let(:test_file_name) { 'test+.jpg' } 137 | 138 | it { is_expected.to eq("#{asset_host.call}/uploads/tmp/#{cache_id}/thumb_test%2B.jpg") } 139 | end 140 | end 141 | 142 | context "when set as nil" do 143 | let(:asset_host) { nil } 144 | 145 | context "when base_path is set" do 146 | let(:base_path) { "/base_path" } 147 | 148 | before do 149 | uploader.class.configure do |config| 150 | config.base_path = base_path 151 | end 152 | end 153 | 154 | it "prepends the config option 'base_path'" do 155 | is_expected.to eq("#{base_path}/uploads/tmp/#{cache_id}/thumb_#{test_file_name}") 156 | end 157 | end 158 | end 159 | end 160 | end 161 | 162 | context "when the version is nested" do 163 | subject { uploader.url(:thumb, :mini) } 164 | 165 | before do 166 | MyCoolUploader.version(:thumb) { version(:mini) } 167 | uploader.cache!(test_file) 168 | end 169 | 170 | it "gets the directory relative to public for a nested version" do 171 | is_expected.to eq("/uploads/tmp/#{cache_id}/thumb_mini_#{test_file_name}") 172 | end 173 | end 174 | end 175 | end 176 | 177 | describe '#to_json' do 178 | subject(:parsed_json) { JSON.parse(to_json) } 179 | 180 | let(:to_json) { uploader.to_json } 181 | 182 | context "(:thumb)" do 183 | before { MyCoolUploader.version(:thumb) } 184 | 185 | it { expect(parsed_json.keys).to include("url") } 186 | it { expect(parsed_json.keys).to include("thumb") } 187 | it { expect(parsed_json["url"]).to be_nil } 188 | it { expect(parsed_json["thumb"].keys).to include("url") } 189 | it { expect(parsed_json["thumb"]["url"]).to be_nil } 190 | 191 | context "with a cached_file" do 192 | before { uploader.cache!(test_file) } 193 | 194 | it { expect(parsed_json.keys).to include("thumb") } 195 | it { expect(parsed_json["thumb"]).to eq({"url" => "/uploads/tmp/#{cache_id}/thumb_#{test_file_name}"}) } 196 | end 197 | end 198 | 199 | context "with cached file" do 200 | before { uploader.cache!(test_file) } 201 | 202 | it "returns a hash including a cached URL" do 203 | is_expected.to eq({"url" => "/uploads/tmp/#{cache_id}/#{test_file_name}"}) 204 | end 205 | end 206 | 207 | it "allows an options parameter to be passed in" do 208 | expect { uploader.to_json({:some => 'options'}) }.not_to raise_error 209 | end 210 | end 211 | 212 | describe '#to_xml' do 213 | subject(:parsed_xml) { Hash.from_xml(to_xml) } 214 | 215 | let(:to_xml) { uploader.to_xml } 216 | 217 | it "returns a hash with a blank URL" do 218 | is_expected.to eq({"uploader" => {"url" => nil}}) 219 | end 220 | 221 | context "with cached file" do 222 | before { uploader.cache!(test_file) } 223 | 224 | it "returns a hash including a cached URL" do 225 | is_expected.to eq({"uploader" => {"url" => "/uploads/tmp/#{cache_id}/#{test_file_name}"}}) 226 | end 227 | 228 | context "with an array of uploaders" do 229 | let(:to_xml) { [uploader].to_xml } 230 | 231 | it "returns a hash including an array with a cached URL" do 232 | is_expected.to have_value([{"url"=>"/uploads/tmp/#{cache_id}/#{test_file_name}"}]) 233 | end 234 | end 235 | end 236 | 237 | describe "(:thumb)" do 238 | before { MyCoolUploader.version(:thumb) } 239 | 240 | context "with cached file" do 241 | before { uploader.cache!(test_file) } 242 | 243 | it "returns a hash including a cached URL of a version" do 244 | expect(parsed_xml["uploader"]["thumb"]).to eq({"url" => "/uploads/tmp/#{cache_id}/thumb_#{test_file_name}"}) 245 | end 246 | end 247 | end 248 | end 249 | 250 | describe '#to_s' do 251 | subject { uploader.to_s } 252 | 253 | it { is_expected.to eq('') } 254 | 255 | context "with cached file" do 256 | before { uploader.cache!(test_file) } 257 | 258 | it "gets the directory relative to public, prepending a slash" do 259 | is_expected.to eq("/uploads/tmp/#{cache_id}/#{test_file_name}") 260 | end 261 | 262 | describe "File#url" do 263 | before { allow(uploader.file).to receive(:url).and_return(url) } 264 | 265 | context "when present" do 266 | let(:url) { 'http://www.example.com/someurl.jpg' } 267 | 268 | it { is_expected.to eq(url) } 269 | end 270 | end 271 | end 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /spec/uploader/download_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CarrierWave::Uploader::Download do 4 | let(:uploader_class) { Class.new(CarrierWave::Uploader::Base) } 5 | let(:uploader) { uploader_class.new } 6 | let(:cache_id) { '1369894322-345-1234-2255' } 7 | let(:base_url) { "http://www.example.com" } 8 | let(:url) { base_url + "/test.jpg" } 9 | let(:test_file) { File.read(file_path(test_file_name)) } 10 | let(:test_file_name) { "test.jpg" } 11 | let(:unicode_named_file) { File.read(file_path(unicode_filename)) } 12 | let(:unicode_URL) { URI.encode(base_url + "/#{unicode_filename}") } 13 | let(:unicode_filename) { "юникод.jpg" } 14 | let(:authentication_headers) do 15 | { 16 | 'Accept'=>'*/*', 17 | 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 18 | 'User-Agent'=>"CarrierWave/#{CarrierWave::VERSION}", 19 | 'Authorization'=>'Bearer QWE' 20 | } 21 | end 22 | 23 | after { FileUtils.rm_rf(public_path) } 24 | 25 | describe '#download!' do 26 | before do 27 | allow(CarrierWave).to receive(:generate_cache_id).and_return(cache_id) 28 | 29 | stub_request(:get, "www.example.com/#{test_file_name}") 30 | .to_return(body: test_file) 31 | 32 | stub_request(:get, "www.example.com/test-with-no-extension/test"). 33 | to_return(body: test_file, headers: { "Content-Type" => "image/jpeg" }) 34 | 35 | stub_request(:get, "www.example.com/test%20with%20spaces/#{test_file_name}"). 36 | to_return(body: test_file) 37 | 38 | stub_request(:get, "www.example.com/content-disposition"). 39 | to_return(body: test_file, headers: { "Content-Disposition" => 'filename="another_test.jpg"' }) 40 | 41 | stub_request(:get, "www.redirect.com"). 42 | to_return(status: 301, body: "Redirecting", headers: { "Location" => url }) 43 | 44 | stub_request(:get, "www.example.com/missing.jpg"). 45 | to_return(status: 404) 46 | 47 | stub_request(:get, "www.example.com/authorization_required.jpg"). 48 | with(:headers => authentication_headers). 49 | to_return(body: test_file) 50 | 51 | stub_request(:get, unicode_URL).to_return(body: unicode_named_file) 52 | end 53 | 54 | context "when a file was downloaded" do 55 | before do 56 | uploader.download!(url) 57 | end 58 | 59 | it "caches a file" do 60 | expect(uploader.file).to be_an_instance_of(CarrierWave::SanitizedFile) 61 | end 62 | 63 | it "'s cached" do 64 | expect(uploader).to be_cached 65 | end 66 | 67 | it "stores the cache name" do 68 | expect(uploader.cache_name).to eq("#{cache_id}/#{test_file_name}") 69 | end 70 | 71 | it "sets the filename to the file's sanitized filename" do 72 | expect(uploader.filename).to eq("#{test_file_name}") 73 | end 74 | 75 | it "moves it to the tmp dir" do 76 | expect(uploader.file.path).to eq(public_path("uploads/tmp/#{cache_id}/#{test_file_name}")) 77 | expect(uploader.file.exists?).to be_truthy 78 | end 79 | 80 | it "sets the url" do 81 | expect(uploader.url).to eq("/uploads/tmp/#{cache_id}/#{test_file_name}") 82 | end 83 | end 84 | 85 | context "with unicode sybmols in URL" do 86 | before do 87 | uploader.download!(unicode_URL) 88 | end 89 | 90 | it "caches a file" do 91 | expect(uploader.file).to be_an_instance_of(CarrierWave::SanitizedFile) 92 | end 93 | 94 | it "sets the filename to the file's decoded sanitized filename" do 95 | expect(uploader.filename).to eq("#{unicode_filename}") 96 | end 97 | 98 | it "moves it to the tmp dir" do 99 | expect(uploader.file.path).to eq(public_path("uploads/tmp/#{cache_id}/#{unicode_filename}")) 100 | expect(uploader.file.exists?).to be_truthy 101 | end 102 | end 103 | 104 | context "with directory permissions set" do 105 | let(:permissions) { 0777 } 106 | 107 | it "sets permissions" do 108 | uploader_class.permissions = permissions 109 | uploader.download!(url) 110 | 111 | expect(uploader).to have_permissions(permissions) 112 | end 113 | 114 | it "sets directory permissions" do 115 | uploader_class.directory_permissions = permissions 116 | uploader.download!(url) 117 | 118 | expect(uploader).to have_directory_permissions(permissions) 119 | end 120 | end 121 | 122 | context 'with request headers' do 123 | it 'pass custom headers to request' do 124 | auth_required_url = 'http://www.example.com/authorization_required.jpg' 125 | uploader.download!(auth_required_url, { 'Authorization' => 'Bearer QWE' }) 126 | expect(uploader.url).to eq("/uploads/tmp/#{cache_id}/authorization_required.jpg") 127 | end 128 | end 129 | 130 | it "raises an error when trying to download a local file" do 131 | expect { uploader.download!('/etc/passwd') }.to raise_error(CarrierWave::DownloadError) 132 | end 133 | 134 | it "raises an error when trying to download a missing file" do 135 | expect{ uploader.download!("#{base_url}/missing.jpg") }.to raise_error(CarrierWave::DownloadError) 136 | end 137 | 138 | it "accepts spaces in the url" do 139 | uploader.download!(url) 140 | expect(uploader.url).to eq("/uploads/tmp/#{cache_id}/#{test_file_name}") 141 | end 142 | 143 | it "follows redirects" do 144 | uploader.download!('http://www.redirect.com/') 145 | expect(uploader.url).to eq("/uploads/tmp/#{cache_id}/#{test_file_name}") 146 | end 147 | 148 | it "reads content-disposition headers" do 149 | uploader.download!("#{base_url}/content-disposition") 150 | expect(uploader.url).to eq("/uploads/tmp/#{cache_id}/another_#{test_file_name}") 151 | end 152 | 153 | it 'sets file extension based on content-type if missing' do 154 | uploader.download!("#{base_url}/test-with-no-extension/test") 155 | 156 | expect(uploader.url).to match %r{/uploads/tmp/#{cache_id}/test\.jp(e|e?g)$} 157 | end 158 | 159 | it "doesn't obscure original exception message" do 160 | expect { uploader.download!("#{base_url}/missing.jpg") }.to raise_error(CarrierWave::DownloadError, /could not download file: 404/) 161 | end 162 | 163 | describe '#download! with an extension_whitelist' do 164 | before do 165 | uploader_class.class_eval do 166 | def extension_whitelist 167 | %w(txt) 168 | end 169 | end 170 | end 171 | 172 | it "follows redirects but still respect the extension_whitelist" do 173 | expect { uploader.download!('http://www.redirect.com/') }.to raise_error(CarrierWave::IntegrityError) 174 | end 175 | 176 | it "reads content-disposition header but still respect the extension_whitelist" do 177 | expect { uploader.download!("#{base_url}/content-disposition") }.to raise_error(CarrierWave::IntegrityError) 178 | end 179 | end 180 | 181 | describe '#download! with an extension_blacklist' do 182 | before do 183 | uploader_class.class_eval do 184 | def extension_blacklist 185 | %w(jpg) 186 | end 187 | end 188 | end 189 | 190 | it "follows redirects but still respect the extension_blacklist" do 191 | expect { uploader.download!('http://www.redirect.com/') }.to raise_error(CarrierWave::IntegrityError) 192 | end 193 | 194 | it "reads content-disposition header but still respect the extension_blacklist" do 195 | expect { uploader.download!("#{base_url}/content-disposition") }.to raise_error(CarrierWave::IntegrityError) 196 | end 197 | end 198 | end 199 | 200 | describe '#download! with an overridden process_uri method' do 201 | before do 202 | uploader_class.class_eval do 203 | def process_uri(uri) 204 | raise CarrierWave::DownloadError 205 | end 206 | end 207 | end 208 | 209 | it "allows overriding the process_uri method" do 210 | expect { uploader.download!(url) }.to raise_error(CarrierWave::DownloadError) 211 | end 212 | end 213 | 214 | describe '#process_uri' do 215 | it "parses but not escape already escaped uris" do 216 | uri = 'http://example.com/%5B.jpg' 217 | processed = uploader.process_uri(uri) 218 | expect(processed.class).to eq(URI::HTTP) 219 | expect(processed.to_s).to eq(uri) 220 | end 221 | 222 | it "parses but not escape uris with query-string-only characters not needing escaping" do 223 | uri = 'http://example.com/?foo[]=bar' 224 | processed = uploader.process_uri(uri) 225 | expect(processed.class).to eq(URI::HTTP) 226 | expect(processed.to_s).to eq(uri) 227 | end 228 | 229 | it "escapes and parse unescaped uris" do 230 | uri = 'http://example.com/ %[].jpg' 231 | processed = uploader.process_uri(uri) 232 | expect(processed.class).to eq(URI::HTTP) 233 | expect(processed.to_s).to eq('http://example.com/%20%25%5B%5D.jpg') 234 | end 235 | 236 | it "escapes and parse brackets in uri paths without harming the query string" do 237 | uri = 'http://example.com/].jpg?test[]' 238 | processed = uploader.process_uri(uri) 239 | expect(processed.class).to eq(URI::HTTP) 240 | expect(processed.to_s).to eq('http://example.com/%5D.jpg?test[]') 241 | end 242 | 243 | it "throws an exception on bad uris" do 244 | uri = '~http:' 245 | expect { uploader.process_uri(uri) }.to raise_error(CarrierWave::DownloadError) 246 | end 247 | end 248 | end 249 | --------------------------------------------------------------------------------