diff --git a/Gemfile b/Gemfile index fb117e336..44ab6e82d 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,4 @@ gem "ruby-debug" gem "aws-s3", :require => "aws/s3" gem "sqlite3-ruby", "~>1.3.0" gem "appraisal" +gem 'fog' diff --git a/Gemfile.lock b/Gemfile.lock index d40d5b7e3..d601cd397 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,16 +10,31 @@ GEM xml-simple builder (3.0.0) columnize (0.3.2) + excon (0.5.2) + fog (0.5.2) + builder + excon (>= 0.5.2) + formatador (>= 0.0.16) + json + mime-types + net-ssh (>= 2.0.23) + nokogiri (>= 1.4.4) + ruby-hmac + formatador (0.0.16) + json (1.5.1) linecache (0.43) mime-types (1.16) mocha (0.9.9) rake + net-ssh (2.1.0) + nokogiri (1.4.4) rake (0.8.7) ruby-debug (0.10.4) columnize (>= 0.1) ruby-debug-base (~> 0.10.4.0) ruby-debug-base (0.10.4) linecache (>= 0.3) + ruby-hmac (0.4.0) shoulda (2.11.3) sqlite3-ruby (1.3.2) xml-simple (1.0.12) @@ -30,6 +45,7 @@ PLATFORMS DEPENDENCIES appraisal aws-s3 + fog mocha rake ruby-debug diff --git a/lib/paperclip/storage.rb b/lib/paperclip/storage.rb index a716bbbc8..f1fc67279 100644 --- a/lib/paperclip/storage.rb +++ b/lib/paperclip/storage.rb @@ -1,2 +1,3 @@ require "paperclip/storage/filesystem" +require "paperclip/storage/fog" require "paperclip/storage/s3" diff --git a/lib/paperclip/storage/fog.rb b/lib/paperclip/storage/fog.rb new file mode 100644 index 000000000..a09c972c7 --- /dev/null +++ b/lib/paperclip/storage/fog.rb @@ -0,0 +1,98 @@ +module Paperclip + module Storage + + module Fog + def self.extended base + begin + require 'fog' + rescue LoadError => e + e.message << " (You may need to install the fog gem)" + raise e + end + + base.instance_eval do + @fog_directory = @options[:fog_directory] + @fog_credentials = @options[:fog_credentials] + @fog_host = @options[:fog_host] + @fog_public = @options[:fog_public] + + @url = ':fog_public_url' + Paperclip.interpolates(:fog_public_url) do |attachment, style| + attachment.public_url(style) + end + end + end + + def exists?(style = default_style) + if original_filename + !!directory.files.head(path(style)) + else + false + end + end + + def flush_writes + for style, file in @queued_for_write do + log("saving #{path(style)}") + directory.files.create( + :body => file, + :key => path(style), + :public => @fog_public + ) + end + @queued_for_write = {} + end + + def flush_deletes + for path in @queued_for_delete do + log("deleting #{path}") + directory.files.new(:key => path).destroy + end + @queued_for_delete = [] + end + + # Returns representation of the data of the file assigned to the given + # style, in the format most representative of the current storage. + def to_file(style = default_style) + if @queued_for_write[style] + @queued_for_write[style] + else + body = directory.files.get(path(style)).body + filename = path(style) + extname = File.extname(filename) + basename = File.basename(filename, extname) + file = Tempfile.new([basename, extname]) + file.binmode + file.write(body) + file.rewind + file + end + end + + def public_url(style = default_style) + if @fog_host + "#{@fog_host}/#{path(style)}" + else + directory.files.new(:key => path(style)).public_url + end + end + + private + + def connection + @connection ||= ::Fog::Storage.new(@fog_credentials) + end + + def directory + @directory ||= begin + connection.directories.get(@fog_directory) || connection.directories.create( + :key => @fog_directory, + :public => @fog_public + ) + end + end + + end + + end +end diff --git a/paperclip.gemspec b/paperclip.gemspec index 6e000182b..3bd4007a4 100644 --- a/paperclip.gemspec +++ b/paperclip.gemspec @@ -25,8 +25,8 @@ spec = Gem::Specification.new do |s| s.extra_rdoc_files = Dir["README*"] s.rdoc_options << '--line-numbers' << '--inline-source' s.requirements << "ImageMagick" - s.add_dependency 'activerecord' - s.add_dependency 'activesupport' + s.add_dependency 'activerecord', '~>2.3.0' + s.add_dependency 'activesupport', '=2.3.2' s.add_development_dependency 'shoulda' s.add_development_dependency 'appraisal' s.add_development_dependency 'mocha' diff --git a/test/fog_test.rb b/test/fog_test.rb new file mode 100644 index 000000000..05b4c0fb4 --- /dev/null +++ b/test/fog_test.rb @@ -0,0 +1,107 @@ +require './test/helper' +require 'fog' + +Fog.mock! + +class FogTest < Test::Unit::TestCase + context "" do + + setup do + @fog_directory = 'papercliptests' + + @credentials = { + :provider => 'AWS', + :aws_access_key_id => 'ID', + :aws_secret_access_key => 'SECRET' + } + + @connection = Fog::Storage.new(@credentials) + + rebuild_model( + :fog_directory => @fog_directory, + :fog_credentials => @credentials, + :fog_host => nil, + :fog_public => true, + :path => ":attachment/:basename.:extension", + :storage => :fog + ) + end + + should "be extended by the Fog module" do + assert Dummy.new.avatar.is_a?(Paperclip::Storage::Fog) + end + + context "when assigned" do + setup do + @file = File.new(File.join(File.dirname(__FILE__), 'fixtures', '5k.png'), 'rb') + @dummy = Dummy.new + @dummy.avatar = @file + end + + teardown do + @file.close + directory = @connection.directories.new(:key => @fog_directory) + directory.files.each {|file| file.destroy} + directory.destroy + end + + context "without a bucket" do + should "succeed" do + assert @dummy.save + end + end + + context "with a bucket" do + setup do + @connection.directories.create(:key => @fog_directory) + end + + should "succeed" do + assert @dummy.save + end + end + + context "without a fog_host" do + setup do + rebuild_model( + :fog_directory => @fog_directory, + :fog_credentials => @credentials, + :fog_host => nil, + :fog_public => true, + :path => ":attachment/:basename.:extension", + :storage => :fog + ) + @dummy = Dummy.new + @dummy.avatar = StringIO.new('.') + @dummy.save + end + + should "provide a public url" do + assert !@dummy.avatar.url.nil? + end + end + + context "with a fog_host" do + setup do + rebuild_model( + :fog_directory => @fog_directory, + :fog_credentials => @credentials, + :fog_host => 'http://example.com', + :fog_public => true, + :path => ":attachment/:basename.:extension", + :storage => :fog + ) + @dummy = Dummy.new + @dummy.avatar = StringIO.new('.') + @dummy.save + end + + should "provide a public url" do + assert @dummy.avatar.url =~ /^http:\/\/example\.com\/avatars\/stringio\.txt\?\d*$/ + end + end + + end + + end +end