Skip to content

Serve files without using FileStreamer-like object #1321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2016-01-08 12:24:37 -0600 using RuboCop version 0.35.1.
# on 2016-03-14 21:22:57 +0300 using RuboCop version 0.35.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand All @@ -11,39 +11,39 @@ Lint/NestedMethodDefinition:
Exclude:
- 'lib/grape/util/strict_hash_configuration.rb'

# Offense count: 37
# Offense count: 39
Metrics/AbcSize:
Max: 51
Max: 44

# Offense count: 1
Metrics/BlockNesting:
Max: 4

# Offense count: 5
# Offense count: 6
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 281

# Offense count: 23
# Offense count: 24
Metrics/CyclomaticComplexity:
Max: 14

# Offense count: 759
# Offense count: 853
# Configuration parameters: AllowURI, URISchemes.
Metrics/LineLength:
Max: 215

# Offense count: 45
# Offense count: 48
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 36

# Offense count: 8
# Configuration parameters: CountComments.
Metrics/ModuleLength:
Max: 272
Max: 277

# Offense count: 16
# Offense count: 15
Metrics/PerceivedComplexity:
Max: 16

Expand All @@ -57,12 +57,12 @@ Style/BlockDelimiters:
- 'spec/grape/middleware/versioner/header_spec.rb'
- 'spec/grape/request_spec.rb'

# Offense count: 105
# Offense count: 111
# Configuration parameters: Exclude.
Style/Documentation:
Enabled: false

# Offense count: 7
# Offense count: 6
Style/DoubleNegation:
Exclude:
- 'lib/grape/api.rb'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#### Features

* [#1276](https://github.com/ruby-grape/grape/pull/1276): Replace rack-mount with new router - [@namusyaka](https://github.com/namusyaka).
* [#1321](https://github.com/ruby-grape/grape/pull/1321): Serve files without using FileStreamer-like object - [@lfidnl](https://github.com/lfidnl).
* Your contribution here.

#### Fixes
Expand Down
46 changes: 4 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2437,61 +2437,23 @@ end

Use `body false` to return `204 No Content` without any data or content-type.

You can also set the response to a file-like object with `file`.
Note: Rack will read your entire Enumerable before returning a response. If
you would like to stream the response, see `stream`.
You can also set the response to a file with `file`.

```ruby
class FileStreamer
def initialize(file_path)
@file_path = file_path
end

def each(&blk)
File.open(@file_path, 'rb') do |file|
file.each(10, &blk)
end
end
end

class API < Grape::API
get '/' do
file FileStreamer.new('file.bin')
file '/path/to/file'
end
end
```

If you want a file-like object to be streamed using Rack::Chunked, use `stream`.
If you want a file to be streamed using Rack::Chunked, use `stream`.

```ruby
class API < Grape::API
get '/' do
stream FileStreamer.new('file.bin')
end
end
```

If you want to take advantage of `Rack::Sendfile`, which intercepts responses whose body is
being served from a file and replaces it with a server specific X-Sendfile header, specify `to_path`
method in your file streamer class which returns path of served file:

```ruby
class FileStreamer
# ...

def to_path
@file_path
stream '/path/to/file'
end

# ...
end
```

Note: don't forget turn on `Rack::Sendfile` middleware in your API:

```ruby
class API < Grape::API
use Rack::Sendfile
end
```

Expand Down
40 changes: 40 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@ TwitterAPI::routes[0].settings[:custom] # => { key: 'value' }
TwitterAPI::routes[0].request_method # => 'GET'
```

#### `file` method accepts path to file

Now to serve files via Grape just pass the path to the file. Functionality with FileStreamer-like objects is deprecated.

Please, replace your FileStreamer-like objects with paths of served files.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Care to provide an example here? Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem :)


Old style:

```ruby
class FileStreamer
def initialize(file_path)
@file_path = file_path
end

def each(&blk)
File.open(@file_path, 'rb') do |file|
file.each(10, &blk)
end
end
end

# ...

class API < Grape::API
get '/' do
file FileStreamer.new('/path/to/file')
end
end
```

New style:

```ruby
class API < Grape::API
get '/' do
file '/path/to/file'
end
end
```

### Upgrading to >= 0.15.0

#### Changes to availability of `:with` option of `rescue_from` method
Expand Down
9 changes: 7 additions & 2 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ module Util
autoload :StackableValues
autoload :InheritableSetting
autoload :StrictHashConfiguration
autoload :FileResponse
autoload :SendfileResponse
end

module DSL
Expand Down Expand Up @@ -162,6 +160,13 @@ module Presenters
extend ActiveSupport::Autoload
autoload :Presenter
end

module ServeFile
extend ActiveSupport::Autoload
autoload :FileResponse
autoload :FileBody
autoload :SendfileResponse
end
end

require 'grape/util/content_types'
Expand Down
8 changes: 6 additions & 2 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,12 @@ def body(value = nil)
#
# GET /file # => "contents of file"
def file(value = nil)
if value
@file = Grape::Util::FileResponse.new(value)
if value.is_a?(String)
file_body = Grape::ServeFile::FileBody.new(value)
@file = Grape::ServeFile::FileResponse.new(file_body)
elsif !value.is_a?(NilClass)
warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.'
@file = Grape::ServeFile::FileResponse.new(value)
else
@file
end
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def after
def build_formatted_response(status, headers, bodies)
headers = ensure_content_type(headers)

if bodies.is_a?(Grape::Util::FileResponse)
Grape::Util::SendfileResponse.new([], status, headers) do |resp|
if bodies.is_a?(Grape::ServeFile::FileResponse)
Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp|
resp.body = bodies.file
end
else
Expand Down
34 changes: 34 additions & 0 deletions lib/grape/serve_file/file_body.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Grape
module ServeFile
CHUNK_SIZE = 16_384

# Class helps send file through API
class FileBody
attr_reader :path

# @param path [String]
def initialize(path)
@path = path
end

# Need for Rack::Sendfile middleware
#
# @return [String]
def to_path
path
end

def each
File.open(path, 'rb') do |file|
while (chunk = file.read(CHUNK_SIZE))
yield chunk
end
end
end

def ==(other)
path == other.path
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Grape
module Util
module ServeFile
# A simple class used to identify responses which represent files and do not
# need to be formatted or pre-read by Rack::Response
class FileResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Grape
module Util
module ServeFile
# Response should respond to to_path method
# for using Rack::SendFile middleware
class SendfileResponse < Rack::Response
Expand Down
41 changes: 34 additions & 7 deletions spec/grape/dsl/inside_route_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,37 @@ def initialize

describe '#file' do
describe 'set' do
before do
subject.file 'file'
context 'as file path' do
let(:file_path) { '/some/file/path' }

let(:file_response) do
file_body = Grape::ServeFile::FileBody.new(file_path)
Grape::ServeFile::FileResponse.new(file_body)
end

before do
subject.file file_path
end

it 'returns value wrapped in FileResponse' do
expect(subject.file).to eq file_response
end
end

it 'returns value wrapped in FileResponse' do
expect(subject.file).to eq Grape::Util::FileResponse.new('file')
context 'as object (backward compatibility)' do
let(:file_object) { Class.new }

let(:file_response) do
Grape::ServeFile::FileResponse.new(file_object)
end

before do
subject.file file_object
end

it 'returns value wrapped in FileResponse' do
expect(subject.file).to eq file_response
end
end
end

Expand All @@ -195,19 +220,21 @@ def initialize

describe '#stream' do
describe 'set' do
let(:file_object) { Class.new }

before do
subject.header 'Cache-Control', 'cache'
subject.header 'Content-Length', 123
subject.header 'Transfer-Encoding', 'base64'
subject.stream 'file'
subject.stream file_object
end

it 'returns value wrapped in FileResponse' do
expect(subject.stream).to eq Grape::Util::FileResponse.new('file')
expect(subject.stream).to eq Grape::ServeFile::FileResponse.new(file_object)
end

it 'also sets result of file to value wrapped in FileResponse' do
expect(subject.file).to eq Grape::Util::FileResponse.new('file')
expect(subject.file).to eq Grape::ServeFile::FileResponse.new(file_object)
end

it 'sets Cache-Control header to no-cache' do
Expand Down
4 changes: 2 additions & 2 deletions spec/grape/middleware/formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ def to_xml
let(:app) { ->(_env) { [200, {}, @body] } }

it 'returns Grape::Uril::SendFileReponse' do
@body = Grape::Util::FileResponse.new('file')
@body = Grape::ServeFile::FileResponse.new('file')
env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' }
expect(subject.call(env)).to be_a(Grape::Util::SendfileResponse)
expect(subject.call(env)).to be_a(Grape::ServeFile::SendfileResponse)
end
end
end