aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/active_storage/disk_controller.rb26
-rw-r--r--config/routes.rb3
-rw-r--r--lib/active_storage/service/disk_service.rb29
-rw-r--r--test/controllers/direct_uploads_controller_test.rb18
-rw-r--r--test/controllers/disk_controller_test.rb59
-rw-r--r--test/service/shared_service_tests.rb4
-rw-r--r--test/test_helper.rb4
7 files changed, 134 insertions, 9 deletions
diff --git a/app/controllers/active_storage/disk_controller.rb b/app/controllers/active_storage/disk_controller.rb
index ff10cfba84..6be88d2857 100644
--- a/app/controllers/active_storage/disk_controller.rb
+++ b/app/controllers/active_storage/disk_controller.rb
@@ -12,11 +12,26 @@ class ActiveStorage::DiskController < ActionController::Base
end
end
+ def update
+ if token = decode_verified_token
+ if acceptable_content?(token)
+ disk_service.upload token[:key], request.body, checksum: token[:checksum]
+ else
+ head :unprocessable_entity
+ end
+ else
+ head :not_found
+ end
+ rescue ActiveStorage::IntegrityError
+ head :unprocessable_entity
+ end
+
private
def disk_service
ActiveStorage::Blob.service
end
+
def decode_verified_key
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
end
@@ -24,4 +39,15 @@ class ActiveStorage::DiskController < ActionController::Base
def disposition_param
params[:disposition].presence_in(%w( inline attachment )) || "inline"
end
+
+
+ def decode_verified_token
+ ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
+ end
+
+ # FIXME: Validate Content-Length when we're using integration tests. Controller tests don't
+ # populate the header properly when a request body is provided.
+ def acceptable_content?(token)
+ token[:content_type] == request.content_type
+ end
end
diff --git a/config/routes.rb b/config/routes.rb
index b368e35cac..bee0d9d256 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,5 +1,5 @@
Rails.application.routes.draw do
- get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
+ get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
direct :rails_blob do |blob|
route_for(:rails_service_blob, blob.signed_id, blob.filename)
@@ -23,5 +23,6 @@ Rails.application.routes.draw do
get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_blob
+ put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_blob
post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
end
diff --git a/lib/active_storage/service/disk_service.rb b/lib/active_storage/service/disk_service.rb
index 7e2079385f..d473771563 100644
--- a/lib/active_storage/service/disk_service.rb
+++ b/lib/active_storage/service/disk_service.rb
@@ -58,7 +58,7 @@ class ActiveStorage::Service::DiskService < ActiveStorage::Service
verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
generated_url =
- if defined?(Rails) && defined?(Rails.application)
+ if defined?(Rails.application)
Rails.application.routes.url_helpers.rails_disk_blob_path \
verified_key_with_expiration,
disposition: disposition, filename: filename, content_type: content_type
@@ -72,6 +72,32 @@ class ActiveStorage::Service::DiskService < ActiveStorage::Service
end
end
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key do |payload|
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
+ {
+ key: key,
+ content_type: content_type,
+ content_length: content_length,
+ checksum: checksum
+ },
+ expires_in: expires_in,
+ purpose: :blob_token
+ )
+
+ generated_url =
+ if defined?(Rails.application)
+ Rails.application.routes.url_helpers.update_rails_disk_blob_path verified_token_with_expiration
+ else
+ "/rails/active_storage/disk/#{verified_token_with_expiration}"
+ end
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
private
def path_for(key)
File.join root, folder_for(key), key
@@ -87,6 +113,7 @@ class ActiveStorage::Service::DiskService < ActiveStorage::Service
def ensure_integrity_of(key, checksum)
unless Digest::MD5.file(path_for(key)).base64digest == checksum
+ delete key
raise ActiveStorage::IntegrityError
end
end
diff --git a/test/controllers/direct_uploads_controller_test.rb b/test/controllers/direct_uploads_controller_test.rb
index 8f309d0b28..76741d277e 100644
--- a/test/controllers/direct_uploads_controller_test.rb
+++ b/test/controllers/direct_uploads_controller_test.rb
@@ -61,3 +61,21 @@ if SERVICE_CONFIGURATIONS[:gcs]
else
puts "Skipping GCS Direct Upload tests because no GCS configuration was supplied"
end
+
+class ActiveStorage::DiskDirectUploadsControllerTest < ActionController::TestCase
+ setup do
+ @blob = create_blob
+ @routes = Routes
+ @controller = ActiveStorage::DirectUploadsController.new
+ end
+
+ test "creating new direct upload" do
+ post :create, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: Digest::MD5.base64digest("Hello"), content_type: "text/plain" } }
+
+ JSON.parse(@response.body).tap do |details|
+ assert_match /rails\/active_storage\/disk/, details["upload_to_url"]
+ assert_equal "hello.txt", ActiveStorage::Blob.find_signed(details["signed_blob_id"]).filename.to_s
+ end
+ end
+end
diff --git a/test/controllers/disk_controller_test.rb b/test/controllers/disk_controller_test.rb
index 58c56d2d0b..a1542b0784 100644
--- a/test/controllers/disk_controller_test.rb
+++ b/test/controllers/disk_controller_test.rb
@@ -5,20 +5,67 @@ require "active_storage/disk_controller"
class ActiveStorage::DiskControllerTest < ActionController::TestCase
setup do
- @blob = create_blob
@routes = Routes
@controller = ActiveStorage::DiskController.new
end
test "showing blob inline" do
- get :show, params: { filename: @blob.filename, encoded_key: ActiveStorage.verifier.generate(@blob.key, expires_in: 5.minutes, purpose: :blob_key) }
- assert_equal "inline; filename=\"#{@blob.filename}\"", @response.headers["Content-Disposition"]
+ blob = create_blob
+
+ get :show, params: { filename: blob.filename, encoded_key: ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key) }
+ assert_equal "inline; filename=\"#{blob.filename}\"", @response.headers["Content-Disposition"]
assert_equal "text/plain", @response.headers["Content-Type"]
end
- test "sending blob as attachment" do
- get :show, params: { filename: @blob.filename, encoded_key: ActiveStorage.verifier.generate(@blob.key, expires_in: 5.minutes, purpose: :blob_key), disposition: :attachment }
- assert_equal "attachment; filename=\"#{@blob.filename}\"", @response.headers["Content-Disposition"]
+ test "showing blob as attachment" do
+ blob = create_blob
+
+ get :show, params: { filename: blob.filename, encoded_key: ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key), disposition: :attachment }
+ assert_equal "attachment; filename=\"#{blob.filename}\"", @response.headers["Content-Disposition"]
assert_equal "text/plain", @response.headers["Content-Type"]
end
+
+ test "directly uploading blob with integrity" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest(data)
+
+ token = ActiveStorage.verifier.generate(
+ {
+ key: blob.key,
+ content_length: data.size,
+ content_type: "text/plain",
+ checksum: Digest::MD5.base64digest(data)
+ },
+ expires_in: 5.minutes,
+ purpose: :blob_token
+ )
+
+ @request.content_type = "text/plain"
+
+ put :update, body: data, params: { encoded_token: token }
+ assert_response :no_content
+ assert_equal data, blob.download
+ end
+
+ test "directly uploading blob without integrity" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest(data)
+
+ token = ActiveStorage.verifier.generate(
+ {
+ key: blob.key,
+ content_length: data.size,
+ content_type: "text/plain",
+ checksum: Digest::MD5.base64digest("bad data")
+ },
+ expires_in: 5.minutes,
+ purpose: :blob_token
+ )
+
+ @request.content_type = "text/plain"
+
+ put :update, body: data, params: { encoded_token: token }
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
end
diff --git a/test/service/shared_service_tests.rb b/test/service/shared_service_tests.rb
index ad6a9dea7f..07620d91e4 100644
--- a/test/service/shared_service_tests.rb
+++ b/test/service/shared_service_tests.rb
@@ -29,7 +29,7 @@ module ActiveStorage::Service::SharedServiceTests
end
end
- test "upload without integrity" do
+ test "uploading without integrity" do
begin
key = SecureRandom.base58(24)
data = "Something else entirely!"
@@ -37,6 +37,8 @@ module ActiveStorage::Service::SharedServiceTests
assert_raises(ActiveStorage::IntegrityError) do
@service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest("bad data"))
end
+
+ assert_not @service.exist?(key)
ensure
@service.delete key
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d3a55e2cf0..154a2f0835 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -43,6 +43,10 @@ class ActiveSupport::TestCase
filename: filename, content_type: content_type
end
+ def create_blob_before_direct_upload(filename: "hello.txt", byte_size:, checksum:, content_type: "text/plain")
+ ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type
+ end
+
def assert_same_image(fixture_filename, variant)
assert_equal \
File.binread(File.expand_path("../fixtures/files/#{fixture_filename}", __FILE__)),