提交 f0d7ce9e 编写于 作者: D David Heinemeier Hansson 提交者: GitHub

Merge pull request #63 from rails/variants

On-demand variants
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -9,10 +9,7 @@
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"
......
require "action_controller"
require "active_storage/blob"
class ActiveStorage::DirectUploadsController < ActionController::Base
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
......
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
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
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 @@ def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type:
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 @@ def filename
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
......
......@@ -14,17 +14,6 @@ class Engine < Rails::Engine # :nodoc:
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 @@ class Engine < Rails::Engine # :nodoc:
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
......
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
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
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
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)
......
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
$LOAD_PATH << File.expand_path("../../app/controllers", __FILE__)
require "bundler/setup"
require "active_support"
require "active_support/test_case"
......@@ -15,7 +17,6 @@
{}
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 @@
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
......
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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册