diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 36ec3af9525d1..b9a3e1174b685 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,12 @@ +* Add `expires_at` option to `ActiveStorage::Blob#signed_id`. + + ```ruby + rails_blob_path(user.avatar, disposition: "attachment", expires_at: 30.minutes.from_now) + <%= image_tag rails_blob_path(user.avatar.variant(resize: "100x100"), expires_at: 30.minutes.from_now) %> + ``` + + *Aki* + * Allow attaching File and Pathname when assigning attributes, e.g. ```ruby diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index ab1b839a82575..3184ef99908e9 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -158,7 +158,7 @@ def compose(blobs, filename:, content_type: nil, metadata: nil) end # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. - def signed_id(purpose: :blob_id, expires_in: nil) + def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil) super end diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb index 9b36b001164dd..d890a92efc6b6 100644 --- a/activestorage/config/routes.rb +++ b/activestorage/config/routes.rb @@ -32,16 +32,17 @@ direct :rails_storage_proxy do |model, options| expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in } + expires_at = options.delete(:expires_at) if model.respond_to?(:signed_id) route_for( :rails_service_blob_proxy, - model.signed_id(expires_in: expires_in), + model.signed_id(expires_in: expires_in, expires_at: expires_at), model.filename, options ) else - signed_blob_id = model.blob.signed_id(expires_in: expires_in) + signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at) variation_key = model.variation.key filename = model.blob.filename @@ -57,16 +58,17 @@ direct :rails_storage_redirect do |model, options| expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in } + expires_at = options.delete(:expires_at) if model.respond_to?(:signed_id) route_for( :rails_service_blob, - model.signed_id(expires_in: expires_in), + model.signed_id(expires_in: expires_in, expires_at: expires_at), model.filename, options ) else - signed_blob_id = model.blob.signed_id(expires_in: expires_in) + signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at) variation_key = model.variation.key filename = model.blob.filename diff --git a/activestorage/test/controllers/blobs/proxy_controller_test.rb b/activestorage/test/controllers/blobs/proxy_controller_test.rb index c45105ffe4063..6e817c69f6e91 100644 --- a/activestorage/test/controllers/blobs/proxy_controller_test.rb +++ b/activestorage/test/controllers/blobs/proxy_controller_test.rb @@ -32,18 +32,30 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes assert_match(/^attachment; /, response.headers["Content-Disposition"]) end - test "signed ID within expiration date" do + test "signed ID within expiration duration" do get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg"), expires_in: 1.minute) assert_response :success end - test "Expired signed ID" do + test "Expired signed ID within expiration duration" do url = rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg"), expires_in: 1.minute) travel 2.minutes get url assert_response :not_found end + test "signed ID within expiration time" do + get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg"), expires_at: 1.minute.from_now) + assert_response :success + end + + test "Expired signed ID within expiration time" do + url = rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg"), expires_at: 1.minute.from_now) + travel 2.minutes + get url + assert_response :not_found + end + test "single Byte Range" do get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=5-9" } assert_response :partial_content diff --git a/activestorage/test/controllers/blobs/redirect_controller_test.rb b/activestorage/test/controllers/blobs/redirect_controller_test.rb index 339da812b8f8f..e4543b9eb6bc3 100644 --- a/activestorage/test/controllers/blobs/redirect_controller_test.rb +++ b/activestorage/test/controllers/blobs/redirect_controller_test.rb @@ -19,17 +19,29 @@ class ActiveStorage::Blobs::RedirectControllerTest < ActionDispatch::Integration assert_equal "max-age=300, private", response.headers["Cache-Control"] end - test "signed ID within expiration date" do + test "signed ID within expiration duration" do get rails_storage_redirect_url(@blob, expires_in: 1.minute) assert_redirected_to(/racecar\.jpg/) end - test "Expired signed ID" do + test "Expired signed ID within expiration duration" do url = rails_storage_redirect_url(@blob, expires_in: 1.minute) travel 2.minutes get url assert_response :not_found end + + test "signed ID within expiration time" do + get rails_storage_redirect_url(@blob, expires_at: 1.minute.from_now) + assert_redirected_to(/racecar\.jpg/) + end + + test "Expired signed ID within expiration time" do + url = rails_storage_redirect_url(@blob, expires_at: 1.minute.from_now) + travel 2.minutes + get url + assert_response :not_found + end end class ActiveStorage::Blobs::ExpiringRedirectControllerTest < ActionDispatch::IntegrationTest diff --git a/activestorage/test/models/attachment_test.rb b/activestorage/test/models/attachment_test.rb index a4f828ae206db..2e98286b81084 100644 --- a/activestorage/test/models/attachment_test.rb +++ b/activestorage/test/models/attachment_test.rb @@ -108,7 +108,7 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id) end - test "fail to find blob within expiration date" do + test "fail to find blob within expiration duration" do blob = create_blob @user.avatar.attach(blob) @@ -117,6 +117,23 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase assert_nil ActiveStorage::Blob.find_signed(signed_id) end + test "getting a signed blob ID from an attachment with a expires_at" do + blob = create_blob + @user.avatar.attach(blob) + + signed_id = @user.avatar.signed_id(expires_at: 1.minute.from_now) + assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id) + end + + test "fail to find blob within expiration time" do + blob = create_blob + @user.avatar.attach(blob) + + signed_id = @user.avatar.signed_id(expires_at: 1.minute.from_now) + travel 2.minutes + assert_nil ActiveStorage::Blob.find_signed(signed_id) + end + test "signed blob ID backwards compatibility" do blob = create_blob @user.avatar.attach(blob)