mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Squashed commit of the following:
commit 1dca99ad4f8082d8daaa17c6600f3036c25f8e50
Author: Tom Prats <tprats108@gmail.com>
Date: Thu Aug 5 18:07:00 2021 -0400
Moved header
commit 62201870ff5c4124d90912989745819b05d94516
Merge: 5fa3ecae74 c91a8135c7
Author: Tom Prats <tprats108@gmail.com>
Date: Thu Aug 5 14:41:22 2021 -0400
Merge branch 'main' into active-storage-byte-range
commit 5fa3ecae745b4f7c67a6b6b1b7ec420877c96fb8
Author: Tom Prats <tprats108@gmail.com>
Date: Thu Aug 5 14:39:53 2021 -0400
Apply suggestions from code review
Syntax updates
Co-authored-by: Rafael França <rafael@franca.dev>
commit b9553e3698a7af5105171f9d63bd7b89cbb7e2c3
Author: Tom Prats <tprats108@gmail.com>
Date: Wed Jun 23 17:36:26 2021 -0400
Added Active Storage support for byte ranges
This commit is contained in:
parent
7f5ebc4b43
commit
fcc46228c6
5 changed files with 105 additions and 2 deletions
|
@ -1,3 +1,7 @@
|
||||||
|
* Add support for byte range requests
|
||||||
|
|
||||||
|
*Tom Prats*
|
||||||
|
|
||||||
* Attachments can be deleted after their association is no longer defined.
|
* Attachments can be deleted after their association is no longer defined.
|
||||||
|
|
||||||
Fixes #42514
|
Fixes #42514
|
||||||
|
|
|
@ -10,8 +10,15 @@ class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
|
||||||
include ActiveStorage::SetBlob
|
include ActiveStorage::SetBlob
|
||||||
|
|
||||||
def show
|
def show
|
||||||
http_cache_forever public: true do
|
if request.headers["Range"].present?
|
||||||
send_blob_stream @blob
|
send_blob_byte_range_data @blob, request.headers["Range"]
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers["Accept-Ranges"] = "bytes"
|
||||||
|
response.headers["Content-Length"] = @blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream @blob
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,55 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "securerandom"
|
||||||
|
|
||||||
module ActiveStorage::Streaming
|
module ActiveStorage::Streaming
|
||||||
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
|
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
|
||||||
|
|
||||||
|
include ActionController::DataStreaming
|
||||||
include ActionController::Live
|
include ActionController::Live
|
||||||
|
|
||||||
private
|
private
|
||||||
|
# Stream the blob in byte ranges specified through the header
|
||||||
|
def send_blob_byte_range_data(blob, range_header, disposition: nil) #:doc:
|
||||||
|
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
|
||||||
|
|
||||||
|
return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
|
||||||
|
|
||||||
|
if ranges.length == 1
|
||||||
|
range = ranges.first
|
||||||
|
content_type = blob.content_type_for_serving
|
||||||
|
data = blob.download_chunk(range)
|
||||||
|
|
||||||
|
response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{blob.byte_size}"
|
||||||
|
else
|
||||||
|
boundary = SecureRandom.hex
|
||||||
|
content_type = "multipart/byteranges; boundary=#{boundary}"
|
||||||
|
data = +""
|
||||||
|
|
||||||
|
ranges.compact.each do |range|
|
||||||
|
chunk = blob.download_chunk(range)
|
||||||
|
|
||||||
|
data << "\r\n--#{boundary}\r\n"
|
||||||
|
data << "Content-Type: #{blob.content_type_for_serving}\r\n"
|
||||||
|
data << "Content-Range: bytes #{range.begin}-#{range.end}/#{blob.byte_size}\r\n\r\n"
|
||||||
|
data << chunk
|
||||||
|
end
|
||||||
|
|
||||||
|
data << "\r\n--#{boundary}--\r\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
response.headers["Accept-Ranges"] = "bytes"
|
||||||
|
response.headers["Content-Length"] = data.length.to_s
|
||||||
|
|
||||||
|
send_data(
|
||||||
|
data,
|
||||||
|
disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
|
||||||
|
filename: blob.filename.sanitized,
|
||||||
|
status: :partial_content,
|
||||||
|
type: content_type
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
|
# Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
|
||||||
# The content type and filename is set directly from the +blob+.
|
# The content type and filename is set directly from the +blob+.
|
||||||
def send_blob_stream(blob, disposition: nil) #:doc:
|
def send_blob_stream(blob, disposition: nil) #:doc:
|
||||||
|
|
|
@ -253,6 +253,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
||||||
service.download key, &block
|
service.download key, &block
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Downloads a part of the file associated with this blob.
|
||||||
|
def download_chunk(range)
|
||||||
|
service.download_chunk key, range
|
||||||
|
end
|
||||||
|
|
||||||
# Downloads the blob to a tempfile on disk. Yields the tempfile.
|
# Downloads the blob to a tempfile on disk. Yields the tempfile.
|
||||||
#
|
#
|
||||||
# The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
|
# The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
require "database/setup"
|
require "database/setup"
|
||||||
|
require "minitest/mock"
|
||||||
|
|
||||||
class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTest
|
class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTest
|
||||||
test "invalid signed ID" do
|
test "invalid signed ID" do
|
||||||
|
@ -36,6 +37,48 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
|
||||||
get url
|
get url
|
||||||
assert_response :not_found
|
assert_response :not_found
|
||||||
end
|
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
|
||||||
|
assert_equal "5", response.headers["Content-Length"]
|
||||||
|
assert_equal "bytes 5-9/1124062", response.headers["Content-Range"]
|
||||||
|
assert_equal "image/jpeg", response.headers["Content-Type"]
|
||||||
|
assert_equal " Exif", response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid Byte Range" do
|
||||||
|
get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=*/1234" }
|
||||||
|
assert_response :range_not_satisfiable
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multiple Byte Ranges" do
|
||||||
|
boundary = SecureRandom.hex
|
||||||
|
SecureRandom.stub :hex, boundary do
|
||||||
|
get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")), headers: { "Range" => "bytes=5-9,13-17" }
|
||||||
|
assert_response :partial_content
|
||||||
|
assert_equal "252", response.headers["Content-Length"]
|
||||||
|
assert_equal "multipart/byteranges; boundary=#{boundary}", response.headers["Content-Type"]
|
||||||
|
assert_equal(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"--#{boundary}",
|
||||||
|
"Content-Type: image/jpeg",
|
||||||
|
"Content-Range: bytes 5-9/1124062",
|
||||||
|
"",
|
||||||
|
" Exif",
|
||||||
|
"--#{boundary}",
|
||||||
|
"Content-Type: image/jpeg",
|
||||||
|
"Content-Range: bytes 13-17/1124062",
|
||||||
|
"",
|
||||||
|
"I*\u0000\b\u0000",
|
||||||
|
"--#{boundary}--",
|
||||||
|
""
|
||||||
|
].join("\r\n"),
|
||||||
|
response.body
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest
|
class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
Loading…
Reference in a new issue