diff options
-rw-r--r-- | app/controllers/active_storage/disk_controller.rb | 26 | ||||
-rw-r--r-- | config/routes.rb | 3 | ||||
-rw-r--r-- | lib/active_storage/service/disk_service.rb | 29 | ||||
-rw-r--r-- | test/controllers/direct_uploads_controller_test.rb | 18 | ||||
-rw-r--r-- | test/controllers/disk_controller_test.rb | 59 | ||||
-rw-r--r-- | test/service/shared_service_tests.rb | 4 | ||||
-rw-r--r-- | test/test_helper.rb | 4 |
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__)), |