aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2017-07-21 16:51:04 -0500
committerGitHub <noreply@github.com>2017-07-21 16:51:04 -0500
commitf0d7ce9e767ffbf7307ed6efaa4a189ae3ea3c0a (patch)
tree545ce85fc61a1e6bcc75110428648dbf9d59a0ac
parent986a71d26868d296f4c619df85909d1073b6c91f (diff)
parent6ac4fec964e67cf3d7dfbf7726bff9b05aca522c (diff)
downloadrails-f0d7ce9e767ffbf7307ed6efaa4a189ae3ea3c0a.tar.gz
rails-f0d7ce9e767ffbf7307ed6efaa4a189ae3ea3c0a.tar.bz2
rails-f0d7ce9e767ffbf7307ed6efaa4a189ae3ea3c0a.zip
Merge pull request #63 from rails/variants
On-demand variants
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock52
-rw-r--r--README.md8
-rw-r--r--Rakefile1
-rw-r--r--activestorage.gemspec5
-rw-r--r--app/controllers/active_storage/direct_uploads_controller.rb (renamed from lib/active_storage/direct_uploads_controller.rb)3
-rw-r--r--app/controllers/active_storage/disk_controller.rb (renamed from lib/active_storage/disk_controller.rb)0
-rw-r--r--app/controllers/active_storage/variants_controller.rb25
-rw-r--r--config/routes.rb16
-rw-r--r--lib/active_storage/blob.rb9
-rw-r--r--lib/active_storage/engine.rb67
-rw-r--r--lib/active_storage/routes.rb2
-rw-r--r--lib/active_storage/variant.rb35
-rw-r--r--lib/active_storage/variation.rb53
-rw-r--r--lib/active_storage/verified_key_with_expiration.rb2
-rw-r--r--test/controllers/variants_controller.rb24
-rw-r--r--test/fixtures/files/racecar-100x100-monochrome.jpgbin0 -> 27586 bytes
-rw-r--r--test/fixtures/files/racecar-100x100.jpgbin0 -> 29446 bytes
-rw-r--r--test/fixtures/files/racecar.jpgbin0 -> 1124062 bytes
-rw-r--r--test/test_helper.rb22
-rw-r--r--test/variant_test.rb23
21 files changed, 296 insertions, 53 deletions
diff --git a/Gemfile b/Gemfile
index 7154892086..953b85ccfe 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,4 +18,6 @@ gem "httparty"
gem "aws-sdk", "~> 2", require: false
gem "google-cloud-storage", "~> 1.3", require: false
+gem 'mini_magick'
+
gem "rubocop", require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 5b283272f7..e18acab95b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,6 +2,16 @@ GIT
remote: https://github.com/rails/rails.git
revision: 5c16dd35a23f75038baf1527143ee44accf081ff
specs:
+ actioncable (5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ nio4r (~> 2.0)
+ websocket-driver (~> 0.6.1)
+ actionmailer (5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ actionview (= 5.2.0.alpha)
+ activejob (= 5.2.0.alpha)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 2.0)
actionpack (5.2.0.alpha)
actionview (= 5.2.0.alpha)
activesupport (= 5.2.0.alpha)
@@ -29,15 +39,30 @@ GIT
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
+ rails (5.2.0.alpha)
+ actioncable (= 5.2.0.alpha)
+ actionmailer (= 5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ actionview (= 5.2.0.alpha)
+ activejob (= 5.2.0.alpha)
+ activemodel (= 5.2.0.alpha)
+ activerecord (= 5.2.0.alpha)
+ activesupport (= 5.2.0.alpha)
+ bundler (>= 1.3.0)
+ railties (= 5.2.0.alpha)
+ sprockets-rails (>= 2.0.0)
+ railties (5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ activesupport (= 5.2.0.alpha)
+ method_source
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
PATH
remote: .
specs:
activestorage (0.1)
- actionpack (>= 5.2.0.alpha)
- activejob (>= 5.2.0.alpha)
- activerecord (>= 5.2.0.alpha)
- activesupport (>= 5.2.0.alpha)
+ rails (>= 5.2.0.alpha)
GEM
remote: https://rubygems.org/
@@ -101,15 +126,20 @@ GEM
multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
+ mail (2.6.5)
+ mime-types (>= 1.16, < 4)
memoist (0.16.0)
+ method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
+ mini_magick (4.8.0)
mini_portile2 (2.2.0)
minitest (5.10.2)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
+ nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
os (0.9.6)
@@ -147,12 +177,23 @@ GEM
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
+ sprockets (3.7.1)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-rails (3.2.0)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
sqlite3 (1.3.13)
+ thor (0.19.4)
thread_safe (0.3.6)
tzinfo (1.2.3)
thread_safe (~> 0.1)
uber (0.1.0)
unicode-display_width (1.3.0)
+ websocket-driver (0.6.5)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.2)
PLATFORMS
ruby
@@ -168,9 +209,10 @@ DEPENDENCIES
byebug
google-cloud-storage (~> 1.3)
httparty
+ mini_magick
rake
rubocop
sqlite3
BUNDLED WITH
- 1.15.1
+ 1.15.2
diff --git a/README.md b/README.md
index 27678a8fe1..b56999cae7 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,13 @@ class MessagesController < ApplicationController
end
```
+Variation of image attachment:
+
+```erb
+<%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %>
+<%= image_tag url_for(user.avatar.variant(resize: "100x100")) %>
+```
+
## Installation
1. Add `gem "activestorage", git: "https://github.com/rails/activestorage.git"` to your Gemfile.
@@ -70,6 +77,7 @@ end
3. Run `rails activestorage:install` to create needed directories, migrations, and configuration.
4. Configure the storage service in `config/environments/*` with `config.active_storage.service = :local`
that references the services configured in `config/storage_services.yml`.
+5. Optional: Add `gem "mini_magick"` to your Gemfile if you want to use variants.
## Todos
diff --git a/Rakefile b/Rakefile
index f0baf50163..a41e07f373 100644
--- a/Rakefile
+++ b/Rakefile
@@ -3,6 +3,7 @@ require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new do |test|
+ test.libs << "app/controllers"
test.libs << "test"
test.test_files = FileList["test/**/*_test.rb"]
test.warning = false
diff --git a/activestorage.gemspec b/activestorage.gemspec
index 884d3287e6..9546b60783 100644
--- a/activestorage.gemspec
+++ b/activestorage.gemspec
@@ -9,10 +9,7 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 2.3.0"
- s.add_dependency "activesupport", ">= 5.2.0.alpha"
- s.add_dependency "activerecord", ">= 5.2.0.alpha"
- s.add_dependency "actionpack", ">= 5.2.0.alpha"
- s.add_dependency "activejob", ">= 5.2.0.alpha"
+ s.add_dependency "rails", ">= 5.2.0.alpha"
s.add_development_dependency "bundler", "~> 1.15"
diff --git a/lib/active_storage/direct_uploads_controller.rb b/app/controllers/active_storage/direct_uploads_controller.rb
index 99ff27f903..dccd864e8d 100644
--- a/lib/active_storage/direct_uploads_controller.rb
+++ b/app/controllers/active_storage/direct_uploads_controller.rb
@@ -1,6 +1,3 @@
-require "action_controller"
-require "active_storage/blob"
-
class ActiveStorage::DirectUploadsController < ActionController::Base
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
diff --git a/lib/active_storage/disk_controller.rb b/app/controllers/active_storage/disk_controller.rb
index 16a295d00d..16a295d00d 100644
--- a/lib/active_storage/disk_controller.rb
+++ b/app/controllers/active_storage/disk_controller.rb
diff --git a/app/controllers/active_storage/variants_controller.rb b/app/controllers/active_storage/variants_controller.rb
new file mode 100644
index 0000000000..dde7e1458f
--- /dev/null
+++ b/app/controllers/active_storage/variants_controller.rb
@@ -0,0 +1,25 @@
+class ActiveStorage::VariantsController < ActionController::Base
+ def show
+ if blob_key = decode_verified_blob_key
+ redirect_to processed_variant_for(blob_key).url(disposition: disposition_param)
+ else
+ head :not_found
+ end
+ end
+
+ private
+ def decode_verified_blob_key
+ ActiveStorage::VerifiedKeyWithExpiration.decode(params[:encoded_blob_key])
+ end
+
+ def processed_variant_for(blob_key)
+ ActiveStorage::Variant.new(
+ ActiveStorage::Blob.find_by!(key: blob_key),
+ ActiveStorage::Variation.decode(params[:variation_key])
+ ).processed
+ end
+
+ def disposition_param
+ params[:disposition].presence_in(%w( inline attachment )) || 'inline'
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000000..d25f2c82f0
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,16 @@
+Rails.application.routes.draw do
+ get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_blob
+ post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
+
+ get "/rails/active_storage/variants/:encoded_blob_key/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation
+
+ direct :rails_variant do |variant|
+ encoded_blob_key = ActiveStorage::VerifiedKeyWithExpiration.encode(variant.blob.key)
+ variation_key = variant.variation.key
+ filename = variant.blob.filename
+
+ route_for(:rails_blob_variation, encoded_blob_key, variation_key, filename)
+ end
+
+ resolve('ActiveStorage::Variant') { |variant| route_for(:rails_variant, variant) }
+end
diff --git a/lib/active_storage/blob.rb b/lib/active_storage/blob.rb
index 1a15361747..6bd3941cd8 100644
--- a/lib/active_storage/blob.rb
+++ b/lib/active_storage/blob.rb
@@ -1,6 +1,7 @@
require "active_storage/service"
require "active_storage/filename"
require "active_storage/purge_job"
+require "active_storage/variant"
# Schema: id, key, filename, content_type, metadata, byte_size, checksum, created_at
class ActiveStorage::Blob < ActiveRecord::Base
@@ -31,8 +32,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
end
end
- # We can't wait until the record is first saved to have a key for it
+
def key
+ # We can't wait until the record is first saved to have a key for it
self[:key] ||= self.class.generate_unique_secure_token
end
@@ -40,6 +42,11 @@ class ActiveStorage::Blob < ActiveRecord::Base
ActiveStorage::Filename.new(self[:filename])
end
+ def variant(transformations)
+ ActiveStorage::Variant.new(self, ActiveStorage::Variation.new(transformations))
+ end
+
+
def url(expires_in: 5.minutes, disposition: :inline)
service.url key, expires_in: expires_in, disposition: disposition, filename: filename
end
diff --git a/lib/active_storage/engine.rb b/lib/active_storage/engine.rb
index 5f0b62809e..b32ae34516 100644
--- a/lib/active_storage/engine.rb
+++ b/lib/active_storage/engine.rb
@@ -14,17 +14,6 @@ module ActiveStorage
end
end
- initializer "active_storage.routes" do
- require "active_storage/disk_controller"
- require "active_storage/direct_uploads_controller"
-
- config.after_initialize do |app|
- app.routes.prepend do
- eval(File.read(File.expand_path("../routes.rb", __FILE__)))
- end
- end
- end
-
initializer "active_storage.attached" do
require "active_storage/attached"
@@ -33,32 +22,42 @@ module ActiveStorage
end
end
- config.after_initialize do |app|
- if config_choice = app.config.active_storage.service
- config_file = Pathname.new(Rails.root.join("config/storage_services.yml"))
- raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
+ initializer "active_storage.verifiers" do
+ require "active_storage/verified_key_with_expiration"
+ require "active_storage/variation"
- require "yaml"
- require "erb"
+ config.after_initialize do |app|
+ ActiveStorage::VerifiedKeyWithExpiration.verifier = \
+ ActiveStorage::Variation.verifier = \
+ Rails.application.message_verifier('ActiveStorage')
+ end
+ end
+
+ initializer "active_storage.services" do
+ config.after_initialize do |app|
+ if config_choice = app.config.active_storage.service
+ config_file = Pathname.new(Rails.root.join("config/storage_services.yml"))
+ raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
+
+ require "yaml"
+ require "erb"
- configs =
- begin
- YAML.load(ERB.new(config_file.read).result) || {}
- rescue Psych::SyntaxError => e
- raise "YAML syntax error occurred while parsing #{config_file}. " \
- "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
- "Error: #{e.message}"
- end
+ configs =
+ begin
+ YAML.load(ERB.new(config_file.read).result) || {}
+ rescue Psych::SyntaxError => e
+ raise "YAML syntax error occurred while parsing #{config_file}. " \
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
+ "Error: #{e.message}"
+ end
- ActiveStorage::Blob.service =
- begin
- ActiveStorage::Service.configure config_choice, configs
- rescue => e
- raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
- end
- else
- raise "No storage service specified for current env (#{Rails.env}). " \
- "Add config.active_storage.service = :local into your config/environments/#{Rails.env}.rb."
+ ActiveStorage::Blob.service =
+ begin
+ ActiveStorage::Service.configure config_choice, configs
+ rescue => e
+ raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
+ end
+ end
end
end
end
diff --git a/lib/active_storage/routes.rb b/lib/active_storage/routes.rb
deleted file mode 100644
index 748427a776..0000000000
--- a/lib/active_storage/routes.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_blob
-post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
diff --git a/lib/active_storage/variant.rb b/lib/active_storage/variant.rb
new file mode 100644
index 0000000000..435033f980
--- /dev/null
+++ b/lib/active_storage/variant.rb
@@ -0,0 +1,35 @@
+require "active_storage/blob"
+require "mini_magick"
+
+# Image blobs can have variants that are the result of a set of transformations applied to the original.
+class ActiveStorage::Variant
+ attr_reader :blob, :variation
+ delegate :service, to: :blob
+
+ def initialize(blob, variation)
+ @blob, @variation = blob, variation
+ end
+
+ def processed
+ process unless service.exist?(key)
+ self
+ end
+
+ def key
+ "variants/#{blob.key}/#{variation.key}"
+ end
+
+ def url(expires_in: 5.minutes, disposition: :inline)
+ service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename
+ end
+
+
+ private
+ def process
+ service.upload key, transform(service.download(blob.key))
+ end
+
+ def transform(io)
+ File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path
+ end
+end
diff --git a/lib/active_storage/variation.rb b/lib/active_storage/variation.rb
new file mode 100644
index 0000000000..abff288ac1
--- /dev/null
+++ b/lib/active_storage/variation.rb
@@ -0,0 +1,53 @@
+require "active_support/core_ext/object/inclusion"
+
+# A set of transformations that can be applied to a blob to create a variant.
+class ActiveStorage::Variation
+ class_attribute :verifier
+
+ ALLOWED_TRANSFORMATIONS = %i(
+ resize rotate format flip fill monochrome orient quality roll scale sharpen shave shear size thumbnail
+ transparent transpose transverse trim background bordercolor compress crop
+ )
+
+ attr_reader :transformations
+
+ class << self
+ def decode(key)
+ new verifier.verify(key)
+ end
+
+ def encode(transformations)
+ verifier.generate(transformations)
+ end
+ end
+
+ def initialize(transformations)
+ @transformations = transformations
+ end
+
+ def transform(image)
+ transformations.each do |(method, argument)|
+ next unless eligible_transformation?(method)
+
+ if eligible_argument?(argument)
+ image.public_send(method, argument)
+ else
+ image.public_send(method)
+ end
+ end
+ end
+
+ def key
+ self.class.encode(transformations)
+ end
+
+ private
+ def eligible_transformation?(method)
+ method.to_sym.in?(ALLOWED_TRANSFORMATIONS)
+ end
+
+ # FIXME: Consider whitelisting allowed arguments as well?
+ def eligible_argument?(argument)
+ argument.present? && argument != true
+ end
+end
diff --git a/lib/active_storage/verified_key_with_expiration.rb b/lib/active_storage/verified_key_with_expiration.rb
index e429ee21ce..4a46483db5 100644
--- a/lib/active_storage/verified_key_with_expiration.rb
+++ b/lib/active_storage/verified_key_with_expiration.rb
@@ -1,5 +1,5 @@
class ActiveStorage::VerifiedKeyWithExpiration
- class_attribute :verifier, default: defined?(Rails) ? Rails.application.message_verifier("ActiveStorage") : nil
+ class_attribute :verifier
class << self
def encode(key, expires_in: nil)
diff --git a/test/controllers/variants_controller.rb b/test/controllers/variants_controller.rb
new file mode 100644
index 0000000000..6753584d4d
--- /dev/null
+++ b/test/controllers/variants_controller.rb
@@ -0,0 +1,24 @@
+require "test_helper"
+require "database/setup"
+
+require "active_storage/variants_controller"
+require "active_storage/verified_key_with_expiration"
+
+class ActiveStorage::VariantsControllerTest < ActionController::TestCase
+ setup do
+ @routes = Routes
+ @controller = ActiveStorage::VariantsController.new
+
+ @blob = create_image_blob filename: "racecar.jpg"
+ end
+
+ test "showing variant inline" do
+ get :show, params: {
+ filename: @blob.filename,
+ encoded_blob_key: ActiveStorage::VerifiedKeyWithExpiration.encode(@blob.key, expires_in: 5.minutes),
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100") }
+
+ assert_redirected_to /racecar.jpg\?disposition=inline/
+ assert_same_image "racecar-100x100.jpg", @blob.variant(resize: "100x100")
+ end
+end
diff --git a/test/fixtures/files/racecar-100x100-monochrome.jpg b/test/fixtures/files/racecar-100x100-monochrome.jpg
new file mode 100644
index 0000000000..39e683747e
--- /dev/null
+++ b/test/fixtures/files/racecar-100x100-monochrome.jpg
Binary files differ
diff --git a/test/fixtures/files/racecar-100x100.jpg b/test/fixtures/files/racecar-100x100.jpg
new file mode 100644
index 0000000000..2a515a4912
--- /dev/null
+++ b/test/fixtures/files/racecar-100x100.jpg
Binary files differ
diff --git a/test/fixtures/files/racecar.jpg b/test/fixtures/files/racecar.jpg
new file mode 100644
index 0000000000..934b4caa22
--- /dev/null
+++ b/test/fixtures/files/racecar.jpg
Binary files differ
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 03593b12c7..20b22049b3 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,3 +1,5 @@
+$LOAD_PATH << File.expand_path("../../app/controllers", __FILE__)
+
require "bundler/setup"
require "active_support"
require "active_support/test_case"
@@ -15,7 +17,6 @@ rescue Errno::ENOENT
{}
end
-
require "active_storage/service/disk_service"
require "tmpdir"
ActiveStorage::Blob.service = ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests"))
@@ -24,20 +25,35 @@ ActiveStorage::Service.logger = ActiveSupport::Logger.new(STDOUT)
require "active_storage/verified_key_with_expiration"
ActiveStorage::VerifiedKeyWithExpiration.verifier = ActiveSupport::MessageVerifier.new("Testing")
+require "active_storage/variation"
+ActiveStorage::Variation.verifier = ActiveSupport::MessageVerifier.new("Testing")
+
class ActiveSupport::TestCase
private
def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain")
ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type
end
+
+ def create_image_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ ActiveStorage::Blob.create_after_upload! \
+ io: File.open(File.expand_path("../fixtures/files/#{filename}", __FILE__)),
+ filename: filename, content_type: content_type
+ end
+
+ def assert_same_image(fixture_filename, variant)
+ assert_equal \
+ File.binread(File.expand_path("../fixtures/files/#{fixture_filename}", __FILE__)),
+ File.binread(variant.service.send(:path_for, variant.key))
+ end
end
require "action_controller"
require "action_controller/test_case"
-
class ActionController::TestCase
Routes = ActionDispatch::Routing::RouteSet.new.tap do |routes|
routes.draw do
- eval(File.read(File.expand_path("../../lib/active_storage/routes.rb", __FILE__)))
+ # FIXME: Hacky way to avoid having to instantiate the real engine
+ eval(File.readlines(File.expand_path("../../config/routes.rb", __FILE__)).slice(1..-2).join("\n"))
end
end
end
diff --git a/test/variant_test.rb b/test/variant_test.rb
new file mode 100644
index 0000000000..5294b87135
--- /dev/null
+++ b/test/variant_test.rb
@@ -0,0 +1,23 @@
+require "test_helper"
+require "database/setup"
+require "active_storage/variant"
+
+class ActiveStorage::VariantTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_image_blob filename: "racecar.jpg"
+ end
+
+ test "resized variation" do
+ variant = @blob.variant(resize: "100x100").processed
+
+ assert_match /racecar.jpg/, variant.url
+ assert_same_image "racecar-100x100.jpg", variant
+ end
+
+ test "resized and monochrome variation" do
+ variant = @blob.variant(resize: "100x100", monochrome: true).processed
+
+ assert_match /racecar.jpg/, variant.url
+ assert_same_image "racecar-100x100-monochrome.jpg", variant
+ end
+end