Skip to content

Commit

Permalink
[WIP] Handle byte range requests for local files (#2816)
Browse files Browse the repository at this point in the history
Handle byte range requests for local files
  • Loading branch information
cjcolvar authored Mar 29, 2018
1 parent 948b67b commit 322d04f
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 4 deletions.
1 change: 1 addition & 0 deletions .rubocop_fixme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Metrics/ClassLength:
Exclude:
- 'app/controllers/hyrax/dashboard/collections_controller.rb'
- 'app/controllers/hyrax/admin/admin_sets_controller.rb'
- 'app/controllers/hyrax/downloads_controller.rb'
- 'app/controllers/hyrax/file_sets_controller.rb'
- 'app/forms/hyrax/forms/permission_template_form.rb'
- 'app/presenters/hyrax/work_show_presenter.rb'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module Hyrax
module LocalFileDownloadsControllerBehavior
protected

# Handle the HTTP show request
def send_local_content
response.headers['Accept-Ranges'] = 'bytes'
if request.head?
local_content_head
elsif request.headers['Range']
send_range_for_local_file
else
send_local_file_contents
end
end

# render an HTTP Range response
def send_range_for_local_file
_, range = request.headers['Range'].split('bytes=')
from, to = range.split('-').map(&:to_i)
to = local_file_size - 1 unless to
length = to - from + 1
response.headers['Content-Range'] = "bytes #{from}-#{to}/#{local_file_size}"
response.headers['Content-Length'] = length.to_s
self.status = 206
prepare_local_file_headers
# For derivatives stored on the local file system
send_data IO.binread(file, length, from), local_derivative_download_options.merge(status: status)
end

def send_local_file_contents
self.status = 200
prepare_local_file_headers
# For derivatives stored on the local file system
send_file file, local_derivative_download_options
end

def local_file_size
File.size(file)
end

def local_file_mime_type
mime_type_for(file)
end

# @return [String] the filename
def local_file_name
params[:filename] || File.basename(file) || (asset.respond_to?(:label) && asset.label)
end

def local_file_last_modified
File.mtime(file) if file.is_a? String
end

# Override
# render an HTTP HEAD response
def local_content_head
response.headers['Content-Length'] = local_file_size.to_s
head :ok, content_type: local_file_mime_type
end

# Override
def prepare_local_file_headers
send_file_headers! local_content_options
response.headers['Content-Type'] = local_file_mime_type
response.headers['Content-Length'] ||= local_file_size.to_s
# Prevent Rack::ETag from calculating a digest over body
response.headers['Last-Modified'] = local_file_last_modified.utc.strftime("%a, %d %b %Y %T GMT")
self.content_type = local_file_mime_type
end

private

# Override the Hydra::Controller::DownloadBehavior#content_options so that
# we have an attachement rather than 'inline'
def local_content_options
{ type: local_file_mime_type, filename: local_file_name, disposition: 'attachment' }
end

# Override this method if you want to change the options sent when downloading
# a derivative file
def local_derivative_download_options
{ type: local_file_mime_type, filename: local_file_name, disposition: 'inline' }
end
end
end
7 changes: 3 additions & 4 deletions app/controllers/hyrax/downloads_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Hyrax
class DownloadsController < ApplicationController
include Hydra::Controller::DownloadBehavior
include Hyrax::LocalFileDownloadsControllerBehavior

def self.default_content_path
:original_file
Expand All @@ -15,9 +16,7 @@ def show
super
when String
# For derivatives stored on the local file system
response.headers['Accept-Ranges'] = 'bytes'
response.headers['Content-Length'] = File.size(file).to_s
send_file file, derivative_download_options
send_local_content
else
raise ActiveFedora::ObjectNotFoundError
end
Expand Down Expand Up @@ -55,7 +54,7 @@ def default_image
# Loads the file specified by the HTTP parameter `:file`.
# If this object does not have a file by that name, return the default file
# as returned by {#default_file}
# @return [ActiveFedora::File, String, NilClass] Returns the file from the repository or a path to a file on the local file system, if it exists.
# @return [ActiveFedora::File, File, NilClass] Returns the file from the repository or a path to a file on the local file system, if it exists.
def load_file
file_reference = params[:file]
return default_file unless file_reference
Expand Down
42 changes: 42 additions & 0 deletions spec/controllers/hyrax/downloads_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,48 @@
expect(ActiveFedora::Base).not_to receive(:find).with(file_set.id)
get :show, params: { id: file_set, file: 'thumbnail' }
end

context "stream" do
it "head request" do
request.env["HTTP_RANGE"] = 'bytes=0-15'
head :show, params: { id: file_set, file: 'thumbnail' }
expect(response.headers['Content-Length']).to eq '4218'
expect(response.headers['Accept-Ranges']).to eq 'bytes'
expect(response.headers['Content-Type']).to start_with 'image/png'
end

it "sends the whole thing" do
request.env["HTTP_RANGE"] = 'bytes=0-4217'
get :show, params: { id: file_set, file: 'thumbnail' }
expect(response.headers["Content-Range"]).to eq 'bytes 0-4217/4218'
expect(response.headers["Content-Length"]).to eq '4218'
expect(response.headers['Accept-Ranges']).to eq 'bytes'
expect(response.headers['Content-Type']).to start_with "image/png"
expect(response.headers["Content-Disposition"]).to eq "inline; filename=\"world.png\""
expect(response.body).to eq content
expect(response.status).to eq 206
end

it "sends the whole thing when the range is open ended" do
request.env["HTTP_RANGE"] = 'bytes=0-'
get :show, params: { id: file_set, file: 'thumbnail' }
expect(response.body).to eq content
end

it "gets a range not starting at the beginning" do
request.env["HTTP_RANGE"] = 'bytes=4200-4217'
get :show, params: { id: file_set, file: 'thumbnail' }
expect(response.headers["Content-Range"]).to eq 'bytes 4200-4217/4218'
expect(response.headers["Content-Length"]).to eq '18'
end

it "gets a range not ending at the end" do
request.env["HTTP_RANGE"] = 'bytes=4-11'
get :show, params: { id: file_set, file: 'thumbnail' }
expect(response.headers["Content-Range"]).to eq 'bytes 4-11/4218'
expect(response.headers["Content-Length"]).to eq '8'
end
end
end

context "that isn't persisted" do
Expand Down

0 comments on commit 322d04f

Please sign in to comment.