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:
Tom Prats 2021-08-05 18:12:07 -04:00
parent 7f5ebc4b43
commit fcc46228c6
5 changed files with 105 additions and 2 deletions

View File

@ -1,3 +1,7 @@
* Add support for byte range requests
*Tom Prats*
* Attachments can be deleted after their association is no longer defined.
Fixes #42514

View File

@ -10,8 +10,15 @@ class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
http_cache_forever public: true do
send_blob_stream @blob
if request.headers["Range"].present?
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

View File

@ -1,11 +1,55 @@
# frozen_string_literal: true
require "securerandom"
module ActiveStorage::Streaming
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
include ActionController::DataStreaming
include ActionController::Live
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+.
# The content type and filename is set directly from the +blob+.
def send_blob_stream(blob, disposition: nil) #:doc:

View File

@ -253,6 +253,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
service.download key, &block
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.
#
# The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.

View File

@ -2,6 +2,7 @@
require "test_helper"
require "database/setup"
require "minitest/mock"
class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTest
test "invalid signed ID" do
@ -36,6 +37,48 @@ class ActiveStorage::Blobs::ProxyControllerTest < ActionDispatch::IntegrationTes
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
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
class ActiveStorage::Blobs::ExpiringProxyControllerTest < ActionDispatch::IntegrationTest